From 1509da1215718311d955fbb821b7c129488c19a1 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 9 Apr 2020 13:20:08 -0400 Subject: [PATCH 01/19] `@0x/contracts-utils`: Convert more 0.6 contracts --- contracts/utils/contracts/src/v06/interfaces/IOwnableV06.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/utils/contracts/src/v06/interfaces/IOwnableV06.sol b/contracts/utils/contracts/src/v06/interfaces/IOwnableV06.sol index 02297d34ae..54faa35c1f 100644 --- a/contracts/utils/contracts/src/v06/interfaces/IOwnableV06.sol +++ b/contracts/utils/contracts/src/v06/interfaces/IOwnableV06.sol @@ -21,6 +21,10 @@ pragma solidity ^0.6.5; interface IOwnableV06 { + /// @dev The owner of this contract. + /// @return ownerAddress The owner address. + function owner() external view returns (address ownerAddress); + /// @dev Emitted by Ownable when ownership is transferred. /// @param previousOwner The previous owner of the contract. /// @param newOwner The new owner of the contract. From 46d5f42c9d71d5763f5cfe68a17cb55b59c01f0f Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 30 Apr 2020 02:59:58 -0400 Subject: [PATCH 02/19] `@0x/utils`: Add new `ZeroExRevertErrors` revert types --- .../zero-ex/puppet_revert_errors.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 packages/utils/src/revert_errors/zero-ex/puppet_revert_errors.ts diff --git a/packages/utils/src/revert_errors/zero-ex/puppet_revert_errors.ts b/packages/utils/src/revert_errors/zero-ex/puppet_revert_errors.ts new file mode 100644 index 0000000000..8885b39612 --- /dev/null +++ b/packages/utils/src/revert_errors/zero-ex/puppet_revert_errors.ts @@ -0,0 +1,41 @@ +import { RevertError } from '../../revert_error'; +import { Numberish } from '../../types'; + +// tslint:disable:max-classes-per-file +export class PuppetExecuteFailedError extends RevertError { + constructor(puppet?: string, callTarget?: string, callData?: string, callValue?: Numberish, errorData?: string) { + super( + 'PuppetExecuteFailedError', + 'PuppetExecuteFailedError(address puppet, address callTarget, bytes callData, uint256 callValue, bytes errorData)', + { + puppet, + callTarget, + callData, + callValue, + errorData, + }, + ); + } +} + +export class PuppetExecuteWithFailedError extends RevertError { + constructor(puppet?: string, callTarget?: string, callData?: string, errorData?: string) { + super( + 'PuppetExecuteWithFailedError', + 'PuppetExecuteWithFailedError(address puppet, address callTarget, bytes callData, bytes errorData)', + { + puppet, + callTarget, + callData, + errorData, + }, + ); + } +} + +const types = [PuppetExecuteFailedError, PuppetExecuteWithFailedError]; + +// Register the types we've defined. +for (const type of types) { + RevertError.registerType(type); +} From 654abbac2573b657d6036863a6e8edf423fd70d8 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 9 Apr 2020 13:20:53 -0400 Subject: [PATCH 03/19] `@0x/contracts-zero-ex`: Introduce the `TransformERC20` feature. --- .../src/v06/interfaces/IOwnableV06.sol | 4 - .../src/errors/LibPuppetRichErrors.sol | 68 +++++++ .../contracts/src/features/IPuppetPool.sol | 55 ++++++ .../contracts/src/features/PuppetPool.sol | 169 ++++++++++++++++++ .../zero-ex/contracts/src/puppets/IPuppet.sol | 45 +++++ .../zero-ex/contracts/src/puppets/Puppet.sol | 132 ++++++++++++++ .../src/storage/LibPuppetPoolStorage.sol | 54 ++++++ .../zero-ex/contracts/test/TestPuppetPool.sol | 31 ++++ .../contracts/test/TestPuppetTarget.sol | 49 +++++ .../zero-ex/test/features/puppet_pool_test.ts | 150 ++++++++++++++++ contracts/zero-ex/test/puppet_test.ts | 117 ++++++++++++ 11 files changed, 870 insertions(+), 4 deletions(-) create mode 100644 contracts/zero-ex/contracts/src/errors/LibPuppetRichErrors.sol create mode 100644 contracts/zero-ex/contracts/src/features/IPuppetPool.sol create mode 100644 contracts/zero-ex/contracts/src/features/PuppetPool.sol create mode 100644 contracts/zero-ex/contracts/src/puppets/IPuppet.sol create mode 100644 contracts/zero-ex/contracts/src/puppets/Puppet.sol create mode 100644 contracts/zero-ex/contracts/src/storage/LibPuppetPoolStorage.sol create mode 100644 contracts/zero-ex/contracts/test/TestPuppetPool.sol create mode 100644 contracts/zero-ex/contracts/test/TestPuppetTarget.sol create mode 100644 contracts/zero-ex/test/features/puppet_pool_test.ts create mode 100644 contracts/zero-ex/test/puppet_test.ts diff --git a/contracts/utils/contracts/src/v06/interfaces/IOwnableV06.sol b/contracts/utils/contracts/src/v06/interfaces/IOwnableV06.sol index 54faa35c1f..02297d34ae 100644 --- a/contracts/utils/contracts/src/v06/interfaces/IOwnableV06.sol +++ b/contracts/utils/contracts/src/v06/interfaces/IOwnableV06.sol @@ -21,10 +21,6 @@ pragma solidity ^0.6.5; interface IOwnableV06 { - /// @dev The owner of this contract. - /// @return ownerAddress The owner address. - function owner() external view returns (address ownerAddress); - /// @dev Emitted by Ownable when ownership is transferred. /// @param previousOwner The previous owner of the contract. /// @param newOwner The new owner of the contract. diff --git a/contracts/zero-ex/contracts/src/errors/LibPuppetRichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibPuppetRichErrors.sol new file mode 100644 index 0000000000..7772f3b310 --- /dev/null +++ b/contracts/zero-ex/contracts/src/errors/LibPuppetRichErrors.sol @@ -0,0 +1,68 @@ +/* + + 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; + + +library LibPuppetRichErrors { + + // solhint-disable func-name-mixedcase + + function PuppetExecuteFailedError( + address puppet, + address callTarget, + bytes memory callData, + uint256 callValue, + bytes memory errorData + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("PuppetExecuteFailedError(address,address,bytes,uint256,bytes)")), + puppet, + callTarget, + callData, + callValue, + errorData + ); + } + + function InvalidPuppetInstanceError(address puppet) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("InvalidPuppetInstanceError(address)")), + puppet + ); + } + + function PuppetNotAcquiredError(address puppet) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("PuppetNotAcquiredError(address)")), + puppet + ); + } +} diff --git a/contracts/zero-ex/contracts/src/features/IPuppetPool.sol b/contracts/zero-ex/contracts/src/features/IPuppetPool.sol new file mode 100644 index 0000000000..400f181d47 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/IPuppetPool.sol @@ -0,0 +1,55 @@ +/* + + 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 "../puppets/IPuppet.sol"; + + +/// @dev Feature to manage a pool of puppet workers. +interface IPuppetPool { + + /// @dev A new puppet contract was created. + /// @param puppet The address of the puppet contract. + event PuppetCreated(address puppet); + + /// @dev Create a new, free puppet to add to the pool. Anyone can call this. + /// @return puppet The new puppet's address. + function createFreePuppet() external returns (address puppet); + + /// @dev Acquire a new puppet instance. This removes the puppet from the + /// pool. If one is not available, a new one will be deployed. + /// Only callable from within. + /// @return puppet The acquired puppet. + function _acquirePuppet() external returns (IPuppet puppet); + + /// @dev Release an acquired puppet instance back into the pool. + /// Only callable from within. + /// @param puppet The puppet to return to the pool. + function _releasePuppet(IPuppet puppet) external; + + /// @dev Gets the number of free puppets in the pool. + /// @return count The number of free puppets in the pool. + function getFreePuppetsCount() external view returns (uint256 count); + + /// @dev Check if an address is a puppet instance. + /// @param puppet The address to check. + /// @return isPuppet_ `true` if `puppet` is a puppet instance. + function isPuppet(address puppet) external view returns (bool isPuppet_); +} diff --git a/contracts/zero-ex/contracts/src/features/PuppetPool.sol b/contracts/zero-ex/contracts/src/features/PuppetPool.sol new file mode 100644 index 0000000000..b82945d773 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/PuppetPool.sol @@ -0,0 +1,169 @@ +/* + + 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/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "../errors/LibPuppetRichErrors.sol"; +import "../fixins/FixinCommon.sol"; +import "../migrations/LibMigrate.sol"; +import "../puppets/IPuppet.sol"; +import "../puppets/Puppet.sol"; +import "../storage/LibPuppetPoolStorage.sol"; +import "./ISimpleFunctionRegistry.sol"; +import "./IPuppetPool.sol"; +import "./IFeature.sol"; + + +/// @dev Feature to manage a pool of puppet workers. +contract PuppetPool is + IFeature, + IPuppetPool, + FixinCommon +{ + // solhint-disable const-name-snakecase + /// @dev Name of this feature. + string constant public override FEATURE_NAME = "PuppetPool"; + /// @dev Version of this feature. + uint256 constant public override FEATURE_VERSION = (1 << 64) | (0 << 32) | (0); + // solhint-enable const-name-snakecase + + /// @dev The implementation address of this feature. + address private immutable _impl; + + using LibSafeMathV06 for uint256; + using LibRichErrorsV06 for bytes; + + constructor() public { + _impl = address(this); + } + + /// @dev Initialize and register this feature. Should be delegatecalled + /// into during a `Migrate.migrate()`. + function migrate() external returns (bytes4 success) { + // Register this feature's functions. + ISimpleFunctionRegistry(address(this)) + .extend(this.createFreePuppet.selector, _impl); + ISimpleFunctionRegistry(address(this)) + .extend(this._acquirePuppet.selector, _impl); + ISimpleFunctionRegistry(address(this)) + .extend(this._releasePuppet.selector, _impl); + ISimpleFunctionRegistry(address(this)) + .extend(this.getFreePuppetsCount.selector, _impl); + ISimpleFunctionRegistry(address(this)) + .extend(this.isPuppet.selector, _impl); + return LibMigrate.MIGRATE_SUCCESS; + } + + /// @dev Create a new, free puppet to add to the pool. Anyone can call this. + /// @return puppet The new puppet's address. + function createFreePuppet() + external + override + returns (address puppet) + { + return address(_createPuppet(LibPuppetPoolStorage.PuppetState.Free)); + } + + /// @dev Acquire a new puppet instance. This removes the puppet from the + /// pool. If one is not available, a new one will be deployed. + /// Only callable from within. + /// @return puppet The acquired puppet. + function _acquirePuppet() + external + override + onlySelf + returns (IPuppet puppet) + { + LibPuppetPoolStorage.Storage storage stor = LibPuppetPoolStorage.getStorage(); + uint256 numFreePuppets = stor.freePuppets.length; + if (numFreePuppets == 0) { + puppet = _createPuppet(LibPuppetPoolStorage.PuppetState.Acquired); + } else { + puppet = stor.freePuppets[numFreePuppets - 1]; + stor.puppetState[address(puppet)] = LibPuppetPoolStorage.PuppetState.Acquired; + stor.freePuppets.pop(); + } + } + + /// @dev Release an acquired puppet instance back into the pool. + /// Only callable from within. + /// @param puppet The puppet to return to the pool. + function _releasePuppet(IPuppet puppet) + external + override + onlySelf + { + LibPuppetPoolStorage.Storage storage stor = LibPuppetPoolStorage.getStorage(); + // Validate puppet state. + LibPuppetPoolStorage.PuppetState state = stor.puppetState[address(puppet)]; + if (state == LibPuppetPoolStorage.PuppetState.Invalid) { + LibPuppetRichErrors.InvalidPuppetInstanceError(address(puppet)).rrevert(); + } else if (state == LibPuppetPoolStorage.PuppetState.Free) { + LibPuppetRichErrors.PuppetNotAcquiredError(address(puppet)).rrevert(); + } + // Return the puppet to the pool. + stor.puppetState[address(puppet)] = LibPuppetPoolStorage.PuppetState.Free; + stor.freePuppets.push(Puppet(address(uint160(address(puppet))))); + } + + /// @dev Gets the number of free puppets in the pool. + /// @return count The number of free puppets in the pool. + function getFreePuppetsCount() + external + override + view + returns (uint256 count) + { + return LibPuppetPoolStorage.getStorage().freePuppets.length; + } + + /// @dev Check if an address is a puppet instance. + /// @param puppet The address to check. + /// @return isPuppet_ `true` if `puppet` is a puppet instance. + function isPuppet(address puppet) + external + override + view + returns (bool isPuppet_) + { + LibPuppetPoolStorage.PuppetState state = + LibPuppetPoolStorage.getStorage().puppetState[address(puppet)]; + return state != LibPuppetPoolStorage.PuppetState.Invalid; + } + + /// @dev Deploy a new puppet instance with the provided state. + /// If `state` is `Free`, this will also add it to the free puppets pool. + /// @param state The state of the puppet. + /// @return puppet The new puppet instance. + function _createPuppet(LibPuppetPoolStorage.PuppetState state) + private + returns (Puppet puppet) + { + LibPuppetPoolStorage.Storage storage stor = LibPuppetPoolStorage.getStorage(); + puppet = new Puppet(); + puppet.addAuthorizedAddress(address(this)); + stor.puppetState[address(puppet)] = state; + if (state == LibPuppetPoolStorage.PuppetState.Free) { + stor.freePuppets.push(puppet); + } + emit PuppetCreated(address(puppet)); + } +} diff --git a/contracts/zero-ex/contracts/src/puppets/IPuppet.sol b/contracts/zero-ex/contracts/src/puppets/IPuppet.sol new file mode 100644 index 0000000000..9fe507edc8 --- /dev/null +++ b/contracts/zero-ex/contracts/src/puppets/IPuppet.sol @@ -0,0 +1,45 @@ +/* + + 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/interfaces/IAuthorizableV06.sol"; + + +/// @dev A contract that can execute arbitrary calls from an authority. +interface IPuppet is + IAuthorizableV06 +{ + /// @dev Execute an arbitrary call. + /// @param target The call target. + /// @param callData The call data. + /// @param value Ether to attach to the call. + /// @return resultData The data returned by the call. + function execute( + address payable target, + bytes calldata callData, + uint256 value + ) + external + payable + returns (bytes memory resultData); + + /// @dev Allows the puppet to receive ETH. + receive() external payable; +} diff --git a/contracts/zero-ex/contracts/src/puppets/Puppet.sol b/contracts/zero-ex/contracts/src/puppets/Puppet.sol new file mode 100644 index 0000000000..d60ceda567 --- /dev/null +++ b/contracts/zero-ex/contracts/src/puppets/Puppet.sol @@ -0,0 +1,132 @@ +/* + + 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/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/AuthorizableV06.sol"; +import "../errors/LibPuppetRichErrors.sol"; +import "./IPuppet.sol"; + + +/// @dev A contract that can execute arbitrary calls from an authority. +contract Puppet is + IPuppet, + AuthorizableV06 +{ + // solhint-disable no-unused-vars,indent,no-empty-blocks + using LibRichErrorsV06 for bytes; + + /// @dev Execute an arbitrary call, forwarding any ether attached and + /// refunding any remaining ether. Only an authority can call this. + /// @param target The call target. + /// @param callData The call data. + /// @param value Ether to attach to the call. + /// @return resultData The data returned by the call. + function execute( + address payable target, + bytes calldata callData, + uint256 value + ) + external + payable + override + onlyAuthorized + returns (bytes memory resultData) + { + bool success; + (success, resultData) = target.call{value: value}(callData); + if (!success) { + LibPuppetRichErrors + .PuppetExecuteFailedError( + address(this), + target, + callData, + value, + resultData + ) + .rrevert(); + } + } + + // solhint-disable state-visibility + /// @dev Allows this contract to receive ether. + receive() external override payable {} + // solhint-enable state-visibility + + /// @dev Destroy this contract. Only callable by the owner. + /// @param ethReceiver A payable recipient of any ETH in this contract. + function die(address payable ethReceiver) external onlyOwner { + selfdestruct(ethReceiver); + } + + /// @dev Signal support for receiving ERC1155 tokens. + /// @param interfaceID The interface ID, as per ERC-165 rules. + /// @return hasSupport `true` if this contract supports an ERC-165 interface. + function supportsInterface(bytes4 interfaceID) + external + pure + returns (bool hasSupport) + { + return interfaceID == this.supportsInterface.selector || + interfaceID == this.onERC1155Received.selector ^ this.onERC1155BatchReceived.selector || + interfaceID == this.tokenFallback.selector; + } + /// @dev Allow this contract to receive ERC1155 tokens. + /// @return success `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + function onERC1155Received( + address, // operator, + address, // from, + uint256, // id, + uint256, // value, + bytes calldata //data + ) + external + pure + returns (bytes4 success) + { + return this.onERC1155Received.selector; + } + + /// @dev Allow this contract to receive ERC1155 tokens. + /// @return success `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + function onERC1155BatchReceived( + address, // operator, + address, // from, + uint256[] calldata, // ids, + uint256[] calldata, // values, + bytes calldata // data + ) + external + pure + returns (bytes4 success) + { + return this.onERC1155BatchReceived.selector; + } + + /// @dev Allows this contract to receive ERC223 tokens. + function tokenFallback( + address, // from, + uint256, // value, + bytes calldata // value + ) + external + pure + {} +} diff --git a/contracts/zero-ex/contracts/src/storage/LibPuppetPoolStorage.sol b/contracts/zero-ex/contracts/src/storage/LibPuppetPoolStorage.sol new file mode 100644 index 0000000000..9eab4edcdb --- /dev/null +++ b/contracts/zero-ex/contracts/src/storage/LibPuppetPoolStorage.sol @@ -0,0 +1,54 @@ +/* + + 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 "./LibStorage.sol"; +import "../puppets/Puppet.sol"; + + +/// @dev Storage helpers for the `PuppetPool` feature. +library LibPuppetPoolStorage { + + /// @dev The state of a puppet instance. + enum PuppetState { + // Not a valid puppet (default) + Invalid, + // Puppet is free to be acquired. + Free, + // Puppet is currently acquired. + Acquired + } + + /// @dev Storage bucket for this feature. + struct Storage { + // State of a puppet instance. + mapping(address => PuppetState) puppetState; + // Free puppet instances. + Puppet[] freePuppets; + } + + /// @dev Get the storage bucket for this contract. + function getStorage() internal pure returns (Storage storage stor) { + uint256 storageOffset = LibStorage.getStorageOffset( + LibStorage.StorageId.PuppetPool + ); + assembly { stor_slot := storageOffset } + } +} diff --git a/contracts/zero-ex/contracts/test/TestPuppetPool.sol b/contracts/zero-ex/contracts/test/TestPuppetPool.sol new file mode 100644 index 0000000000..9390a205a5 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestPuppetPool.sol @@ -0,0 +1,31 @@ +/* + + 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/features/PuppetPool.sol"; + + +contract TestPuppetPool is + PuppetPool +{ + modifier onlySelf() override { + _; + } +} diff --git a/contracts/zero-ex/contracts/test/TestPuppetTarget.sol b/contracts/zero-ex/contracts/test/TestPuppetTarget.sol new file mode 100644 index 0000000000..66b70d04b9 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestPuppetTarget.sol @@ -0,0 +1,49 @@ +/* + + 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; + + +contract TestPuppetTarget { + + event PuppetTargetCalled( + address sender, + bytes data, + uint256 value + ); + + bytes4 private constant MAGIC_BYTES = 0x12345678; + bytes private constant REVERTING_DATA = hex"1337"; + + fallback() external payable { + if (keccak256(msg.data) == keccak256(REVERTING_DATA)) { + revert("TestPuppetTarget/REVERT"); + } + emit PuppetTargetCalled( + msg.sender, + msg.data, + msg.value + ); + bytes4 rval = MAGIC_BYTES; + assembly { + mstore(0, rval) + return(0, 32) + } + } +} diff --git a/contracts/zero-ex/test/features/puppet_pool_test.ts b/contracts/zero-ex/test/features/puppet_pool_test.ts new file mode 100644 index 0000000000..13a245e8f9 --- /dev/null +++ b/contracts/zero-ex/test/features/puppet_pool_test.ts @@ -0,0 +1,150 @@ +import { blockchainTests, constants, expect, verifyEventsFromLogs } from '@0x/contracts-test-utils'; +import { ZeroExRevertErrors } from '@0x/utils'; + +import { artifacts } from '../artifacts'; +import { abis } from '../utils/abis'; +import { fullMigrateAsync } from '../utils/migration'; +import { IPuppetPoolEvents, PuppetContract, PuppetPoolContract, ZeroExContract } from '../wrappers'; + +blockchainTests.resets('PuppetPool feature', env => { + let zeroEx: ZeroExContract; + let feature: PuppetPoolContract; + let unmanagedPuppet: PuppetContract; + + before(async () => { + const [owner] = await env.getAccountAddressesAsync(); + zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, { + puppetPool: (await PuppetContract.deployFrom0xArtifactAsync( + artifacts.TestPuppetPool, + env.provider, + env.txDefaults, + artifacts, + )).address, + }); + feature = new PuppetPoolContract(zeroEx.address, env.provider, env.txDefaults, abis); + unmanagedPuppet = await PuppetContract.deployFrom0xArtifactAsync( + artifacts.Puppet, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + async function acquirePuppetAsync(): Promise { + const puppet = new PuppetContract( + await feature._acquirePuppet().callAsync(), + env.provider, + env.txDefaults, + abis, + ); + await feature._acquirePuppet().awaitTransactionSuccessAsync(); + return puppet; + } + + async function releasePuppetAsync(puppet: string | PuppetContract): Promise { + await feature + ._releasePuppet(typeof puppet === 'string' ? puppet : puppet.address) + .awaitTransactionSuccessAsync(); + } + + describe('_acquirePuppet() and _releasePuppet()', () => { + it('_acquirePuppet() creates a new puppet if none are available', async () => { + const acquiredPuppets = []; + while ((await feature.getFreePuppetsCount().callAsync()).gt(0)) { + acquiredPuppets.push(await acquirePuppetAsync()); + } + const puppetAddress = await feature._acquirePuppet().callAsync(); + const receipt = await feature._acquirePuppet().awaitTransactionSuccessAsync(); + expect(puppetAddress).to.not.eq(constants.NULL_ADDRESS); + verifyEventsFromLogs(receipt.logs, [{ puppet: puppetAddress }], IPuppetPoolEvents.PuppetCreated); + }); + + it('_acquirePuppet() returns a free puppet if available', async () => { + const freePuppetAddess = await feature.createFreePuppet().callAsync(); + await feature.createFreePuppet().awaitTransactionSuccessAsync(); + // Acquire the free puppet. + const puppetAddress = await feature._acquirePuppet().callAsync(); + const receipt = await feature._acquirePuppet().awaitTransactionSuccessAsync(); + expect(await feature.getFreePuppetsCount().callAsync()).to.bignumber.eq(0); + expect(puppetAddress).to.eq(freePuppetAddess); + verifyEventsFromLogs(receipt.logs, [], IPuppetPoolEvents.PuppetCreated); + }); + + it('can release an EXISTING puppet returned by _acquirePuppet()', async () => { + const freePuppetAddess = await feature.createFreePuppet().callAsync(); + await feature.createFreePuppet().awaitTransactionSuccessAsync(); + // Acquire the free puppet. + const puppetAddress = await feature._acquirePuppet().callAsync(); + await feature._acquirePuppet().awaitTransactionSuccessAsync(); + expect(await feature.getFreePuppetsCount().callAsync()).to.bignumber.eq(0); + expect(puppetAddress).to.eq(freePuppetAddess); + await releasePuppetAsync(puppetAddress); + expect(await feature.getFreePuppetsCount().callAsync()).to.bignumber.eq(1); + }); + + it('can acquire and release many puppets', async () => { + const puppets = []; + for (let i = 0; i < 8; ++i) { + puppets.push(await acquirePuppetAsync()); + } + expect(await feature.getFreePuppetsCount().callAsync()).to.bignumber.eq(0); + for (const puppet of puppets) { + await releasePuppetAsync(puppet); + } + expect(await feature.getFreePuppetsCount().callAsync()).to.bignumber.eq(puppets.length); + }); + + it('cannot release a puppet not created by the pool', async () => { + return expect(releasePuppetAsync(unmanagedPuppet)).to.revertWith( + new ZeroExRevertErrors.Puppet.InvalidPuppetInstanceError(unmanagedPuppet.address), + ); + }); + + it('cannot release a free puppet', async () => { + const puppet = await acquirePuppetAsync(); + await releasePuppetAsync(puppet); + return expect(releasePuppetAsync(puppet)).to.revertWith( + new ZeroExRevertErrors.Puppet.PuppetNotAcquiredError(puppet.address), + ); + }); + }); + + describe('createFreePuppet()', () => { + it('creates a free puppet', async () => { + const puppet = await feature.createFreePuppet().callAsync(); + const receipt = await feature.createFreePuppet().awaitTransactionSuccessAsync(); + verifyEventsFromLogs(receipt.logs, [{ puppet }], IPuppetPoolEvents.PuppetCreated); + expect(await feature.isPuppet(puppet).callAsync()).to.eq(true); + expect(await feature.getFreePuppetsCount().callAsync()).to.bignumber.eq(1); + }); + }); + + describe('isPuppet()', () => { + it('returns false for a puppet not created by the pool', async () => { + expect(await feature.isPuppet(unmanagedPuppet.address).callAsync()).to.eq(false); + }); + + it('returns true for an acquired puppet', async () => { + const puppet = await acquirePuppetAsync(); + expect(await feature.isPuppet(puppet.address).callAsync()).to.eq(true); + }); + + it('returns true for a released puppet', async () => { + const puppet = await acquirePuppetAsync(); + await releasePuppetAsync(puppet); + expect(await feature.isPuppet(puppet.address).callAsync()).to.eq(true); + }); + }); + + describe('puppets', () => { + it('puppet is owned by proxy contract', async () => { + const puppet = await acquirePuppetAsync(); + expect(await puppet.owner().callAsync()).to.eq(zeroEx.address); + }); + + it('proxy contract is authorized', async () => { + const puppet = await acquirePuppetAsync(); + expect(await puppet.authorized(zeroEx.address).callAsync()).to.eq(true); + }); + }); +}); diff --git a/contracts/zero-ex/test/puppet_test.ts b/contracts/zero-ex/test/puppet_test.ts new file mode 100644 index 0000000000..190395b18a --- /dev/null +++ b/contracts/zero-ex/test/puppet_test.ts @@ -0,0 +1,117 @@ +import { + blockchainTests, + constants, + expect, + getRandomInteger, + randomAddress, + verifyEventsFromLogs, +} from '@0x/contracts-test-utils'; +import { AuthorizableRevertErrors, hexUtils, StringRevertError, ZeroExRevertErrors } from '@0x/utils'; + +import { artifacts } from './artifacts'; +import { PuppetContract, TestPuppetTargetContract, TestPuppetTargetEvents } from './wrappers'; + +blockchainTests.resets('Puppets', env => { + let owner: string; + let authority: string; + let puppet: PuppetContract; + let puppetTarget: TestPuppetTargetContract; + + before(async () => { + [owner, authority] = await env.getAccountAddressesAsync(); + puppet = await PuppetContract.deployFrom0xArtifactAsync( + artifacts.Puppet, + env.provider, + env.txDefaults, + artifacts, + ); + await puppet.addAuthorizedAddress(authority).awaitTransactionSuccessAsync(); + puppetTarget = await TestPuppetTargetContract.deployFrom0xArtifactAsync( + artifacts.TestPuppetTarget, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + it('non-authority cannot call execute()', async () => { + const notAuthority = randomAddress(); + const tx = puppet + .execute(randomAddress(), hexUtils.random(), getRandomInteger(0, '100e18')) + .callAsync({ from: notAuthority }); + return expect(tx).to.revertWith(new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthority)); + }); + + it('authority can call execute()', async () => { + const targetData = hexUtils.random(128); + const receipt = await puppet + .execute(puppetTarget.address, targetData, constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: authority }); + verifyEventsFromLogs( + receipt.logs, + [ + { + sender: puppet.address, + data: targetData, + value: constants.ZERO_AMOUNT, + }, + ], + TestPuppetTargetEvents.PuppetTargetCalled, + ); + }); + + it('authority can call execute() with attached ETH', async () => { + const targetData = hexUtils.random(128); + const callValue = getRandomInteger(1, '1e18'); + const receipt = await puppet + .execute(puppetTarget.address, targetData, callValue) + .awaitTransactionSuccessAsync({ from: authority, value: callValue }); + verifyEventsFromLogs( + receipt.logs, + [ + { + sender: puppet.address, + data: targetData, + value: callValue, + }, + ], + TestPuppetTargetEvents.PuppetTargetCalled, + ); + }); + + const TARGET_RETURN_VALUE = hexUtils.rightPad('0x12345678'); + + it('puppet returns call result', async () => { + const result = await puppet + .execute(puppetTarget.address, hexUtils.random(128), constants.ZERO_AMOUNT) + .callAsync({ from: authority }); + expect(result).to.eq(TARGET_RETURN_VALUE); + }); + + const REVERTING_DATA = '0x1337'; + + it('puppet wraps call revert', async () => { + const tx = puppet + .execute(puppetTarget.address, REVERTING_DATA, constants.ZERO_AMOUNT) + .callAsync({ from: authority }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.Puppet.PuppetExecuteFailedError( + puppet.address, + puppetTarget.address, + REVERTING_DATA, + constants.ZERO_AMOUNT, + new StringRevertError('TestPuppetTarget/REVERT').encode(), + ), + ); + }); + + it('puppet can receive ETH', async () => { + await env.web3Wrapper.sendTransactionAsync({ + to: puppet.address, + from: owner, + value: 1, + }); + const bal = await env.web3Wrapper.getBalanceInWeiAsync(puppet.address); + expect(bal).to.bignumber.eq(1); + }); +}); From c911c3352c5da9a0aad3f972c0f54ef617c1af1c Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 5 May 2020 21:41:14 -0400 Subject: [PATCH 04/19] `@0x/contracts-zero-ex`: Make `TokenSpender`'s puppet contract a distinct contract type and rename `getTokenSpenderPuppet()` to `getAllowanceTarget()` --- .../src/puppets/ITokenSpenderPuppet.sol | 30 +++++++++++++++++ .../src/puppets/TokenSpenderPuppet.sol | 33 +++++++++++++++++++ contracts/zero-ex/test/wrappers.ts | 2 ++ contracts/zero-ex/tsconfig.json | 2 ++ 4 files changed, 67 insertions(+) create mode 100644 contracts/zero-ex/contracts/src/puppets/ITokenSpenderPuppet.sol create mode 100644 contracts/zero-ex/contracts/src/puppets/TokenSpenderPuppet.sol diff --git a/contracts/zero-ex/contracts/src/puppets/ITokenSpenderPuppet.sol b/contracts/zero-ex/contracts/src/puppets/ITokenSpenderPuppet.sol new file mode 100644 index 0000000000..9982bc4b63 --- /dev/null +++ b/contracts/zero-ex/contracts/src/puppets/ITokenSpenderPuppet.sol @@ -0,0 +1,30 @@ +/* + + 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 "./IPuppet.sol"; + + +/// @dev An alias for IPuppet, to differentiate from puppets used by PuppetPool. +interface ITokenSpenderPuppet is + IPuppet +{ + // solhint-disable no-empty-blocks +} diff --git a/contracts/zero-ex/contracts/src/puppets/TokenSpenderPuppet.sol b/contracts/zero-ex/contracts/src/puppets/TokenSpenderPuppet.sol new file mode 100644 index 0000000000..2858fdc2bb --- /dev/null +++ b/contracts/zero-ex/contracts/src/puppets/TokenSpenderPuppet.sol @@ -0,0 +1,33 @@ +/* + + 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 "./ITokenSpenderPuppet.sol"; +import "./Puppet.sol"; + + +/// @dev A specialized puppet for use exclusively by the TokenSpender. +/// Essentially an alias to differentiate from puppets in PuppetPool. +contract TokenSpenderPuppet is + ITokenSpenderPuppet, + Puppet +{ + // solhint-disable no-empty-blocks +} diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 4e5208d325..303c54a517 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -17,6 +17,7 @@ export * from '../test/generated-wrappers/i_ownable'; export * from '../test/generated-wrappers/i_simple_function_registry'; export * from '../test/generated-wrappers/i_test_simple_function_registry_feature'; export * from '../test/generated-wrappers/i_token_spender'; +export * from '../test/generated-wrappers/i_token_spender_puppet'; export * from '../test/generated-wrappers/i_transform_erc20'; export * from '../test/generated-wrappers/initial_migration'; export * from '../test/generated-wrappers/lib_bootstrap'; @@ -50,5 +51,6 @@ export * from '../test/generated-wrappers/test_token_spender_erc20_token'; export * from '../test/generated-wrappers/test_transform_erc20'; export * from '../test/generated-wrappers/test_zero_ex_feature'; export * from '../test/generated-wrappers/token_spender'; +export * from '../test/generated-wrappers/token_spender_puppet'; export * from '../test/generated-wrappers/transform_erc20'; export * from '../test/generated-wrappers/zero_ex'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index f6e2e1d2bf..a06888b4e8 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -27,6 +27,7 @@ "test/generated-artifacts/ISimpleFunctionRegistry.json", "test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json", "test/generated-artifacts/ITokenSpender.json", + "test/generated-artifacts/ITokenSpenderPuppet.json", "test/generated-artifacts/ITransformERC20.json", "test/generated-artifacts/InitialMigration.json", "test/generated-artifacts/LibBootstrap.json", @@ -60,6 +61,7 @@ "test/generated-artifacts/TestTransformERC20.json", "test/generated-artifacts/TestZeroExFeature.json", "test/generated-artifacts/TokenSpender.json", + "test/generated-artifacts/TokenSpenderPuppet.json", "test/generated-artifacts/TransformERC20.json", "test/generated-artifacts/ZeroEx.json" ], From d9a9bc35e39c4978623e09331aff1f072b0a0068 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 8 May 2020 12:50:33 -0400 Subject: [PATCH 05/19] `@0x/zero-ex`: Rebase and use "slot" instead of "offset" language in storage buckets. --- .../zero-ex/contracts/src/storage/LibPuppetPoolStorage.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/zero-ex/contracts/src/storage/LibPuppetPoolStorage.sol b/contracts/zero-ex/contracts/src/storage/LibPuppetPoolStorage.sol index 9eab4edcdb..e3931863fd 100644 --- a/contracts/zero-ex/contracts/src/storage/LibPuppetPoolStorage.sol +++ b/contracts/zero-ex/contracts/src/storage/LibPuppetPoolStorage.sol @@ -46,9 +46,9 @@ library LibPuppetPoolStorage { /// @dev Get the storage bucket for this contract. function getStorage() internal pure returns (Storage storage stor) { - uint256 storageOffset = LibStorage.getStorageOffset( + uint256 storageSlot = LibStorage.getStorageSlot( LibStorage.StorageId.PuppetPool ); - assembly { stor_slot := storageOffset } + assembly { stor_slot := storageSlot } } } From af454099598cdb512ba329c0eaf8182ceca79348 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 13 May 2020 16:05:08 -0400 Subject: [PATCH 06/19] `@0x/contracts-zero-ex`: Revamp TransformERC20. --- .../src/errors/LibPuppetRichErrors.sol | 25 +- .../src/{puppets => external}/IPuppet.sol | 21 +- .../src/{puppets => external}/Puppet.sol | 51 +++- .../contracts/src/features/IPuppetPool.sol | 55 ---- .../contracts/src/features/PuppetPool.sol | 169 ------------ .../src/puppets/ITokenSpenderPuppet.sol | 30 --- .../src/puppets/TokenSpenderPuppet.sol | 33 --- .../src/storage/LibPuppetPoolStorage.sol | 54 ---- .../zero-ex/contracts/test/TestPuppetPool.sol | 31 --- .../contracts/test/TestPuppetTarget.sol | 2 + .../zero-ex/test/features/puppet_pool_test.ts | 150 ----------- contracts/zero-ex/test/puppet_test.ts | 242 ++++++++++++------ 12 files changed, 235 insertions(+), 628 deletions(-) rename contracts/zero-ex/contracts/src/{puppets => external}/IPuppet.sol (62%) rename contracts/zero-ex/contracts/src/{puppets => external}/Puppet.sol (76%) delete mode 100644 contracts/zero-ex/contracts/src/features/IPuppetPool.sol delete mode 100644 contracts/zero-ex/contracts/src/features/PuppetPool.sol delete mode 100644 contracts/zero-ex/contracts/src/puppets/ITokenSpenderPuppet.sol delete mode 100644 contracts/zero-ex/contracts/src/puppets/TokenSpenderPuppet.sol delete mode 100644 contracts/zero-ex/contracts/src/storage/LibPuppetPoolStorage.sol delete mode 100644 contracts/zero-ex/contracts/test/TestPuppetPool.sol delete mode 100644 contracts/zero-ex/test/features/puppet_pool_test.ts diff --git a/contracts/zero-ex/contracts/src/errors/LibPuppetRichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibPuppetRichErrors.sol index 7772f3b310..5d7e3ff043 100644 --- a/contracts/zero-ex/contracts/src/errors/LibPuppetRichErrors.sol +++ b/contracts/zero-ex/contracts/src/errors/LibPuppetRichErrors.sol @@ -44,25 +44,22 @@ library LibPuppetRichErrors { ); } - function InvalidPuppetInstanceError(address puppet) - internal - pure - returns (bytes memory) - { - return abi.encodeWithSelector( - bytes4(keccak256("InvalidPuppetInstanceError(address)")), - puppet - ); - } - - function PuppetNotAcquiredError(address puppet) + function PuppetExecuteWithFailedError( + address puppet, + address callTarget, + bytes memory callData, + bytes memory errorData + ) internal pure returns (bytes memory) { return abi.encodeWithSelector( - bytes4(keccak256("PuppetNotAcquiredError(address)")), - puppet + bytes4(keccak256("PuppetExecuteWithFailedError(address,address,bytes,bytes)")), + puppet, + callTarget, + callData, + errorData ); } } diff --git a/contracts/zero-ex/contracts/src/puppets/IPuppet.sol b/contracts/zero-ex/contracts/src/external/IPuppet.sol similarity index 62% rename from contracts/zero-ex/contracts/src/puppets/IPuppet.sol rename to contracts/zero-ex/contracts/src/external/IPuppet.sol index 9fe507edc8..e17725b0ee 100644 --- a/contracts/zero-ex/contracts/src/puppets/IPuppet.sol +++ b/contracts/zero-ex/contracts/src/external/IPuppet.sol @@ -19,14 +19,14 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; -import "@0x/contracts-utils/contracts/src/v06/interfaces/IAuthorizableV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/interfaces/IOwnableV06.sol"; -/// @dev A contract that can execute arbitrary calls from an authority. +/// @dev A contract that can execute arbitrary calls from its owner. interface IPuppet is - IAuthorizableV06 + IOwnableV06 { - /// @dev Execute an arbitrary call. + /// @dev Execute an arbitrary call. Only an authority can call this. /// @param target The call target. /// @param callData The call data. /// @param value Ether to attach to the call. @@ -40,6 +40,19 @@ interface IPuppet is payable returns (bytes memory resultData); + /// @dev Execute an arbitrary delegatecall, in the context of this puppet. + /// Only an authority can call this. + /// @param target The call target. + /// @param callData The call data. + /// @return resultData The data returned by the call. + function executeWith( + address payable target, + bytes calldata callData + ) + external + payable + returns (bytes memory resultData); + /// @dev Allows the puppet to receive ETH. receive() external payable; } diff --git a/contracts/zero-ex/contracts/src/puppets/Puppet.sol b/contracts/zero-ex/contracts/src/external/Puppet.sol similarity index 76% rename from contracts/zero-ex/contracts/src/puppets/Puppet.sol rename to contracts/zero-ex/contracts/src/external/Puppet.sol index d60ceda567..2d1d700c2f 100644 --- a/contracts/zero-ex/contracts/src/puppets/Puppet.sol +++ b/contracts/zero-ex/contracts/src/external/Puppet.sol @@ -20,21 +20,20 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; -import "@0x/contracts-utils/contracts/src/v06/AuthorizableV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/OwnableV06.sol"; import "../errors/LibPuppetRichErrors.sol"; import "./IPuppet.sol"; -/// @dev A contract that can execute arbitrary calls from an authority. +/// @dev A contract that can execute arbitrary calls from its owner. contract Puppet is IPuppet, - AuthorizableV06 + OwnableV06 { // solhint-disable no-unused-vars,indent,no-empty-blocks using LibRichErrorsV06 for bytes; - /// @dev Execute an arbitrary call, forwarding any ether attached and - /// refunding any remaining ether. Only an authority can call this. + /// @dev Execute an arbitrary call. Only an authority can call this. /// @param target The call target. /// @param callData The call data. /// @param value Ether to attach to the call. @@ -47,7 +46,7 @@ contract Puppet is external payable override - onlyAuthorized + onlyOwner returns (bytes memory resultData) { bool success; @@ -65,16 +64,39 @@ contract Puppet is } } - // solhint-disable state-visibility + /// @dev Execute an arbitrary delegatecall, in the context of this puppet. + /// Only an authority can call this. + /// @param target The call target. + /// @param callData The call data. + /// @return resultData The data returned by the call. + function executeWith( + address payable target, + bytes calldata callData + ) + external + payable + override + onlyOwner + returns (bytes memory resultData) + { + bool success; + (success, resultData) = target.delegatecall(callData); + if (!success) { + LibPuppetRichErrors + .PuppetExecuteWithFailedError( + address(this), + target, + callData, + resultData + ) + .rrevert(); + } + } + + // solhint-disable /// @dev Allows this contract to receive ether. receive() external override payable {} - // solhint-enable state-visibility - - /// @dev Destroy this contract. Only callable by the owner. - /// @param ethReceiver A payable recipient of any ETH in this contract. - function die(address payable ethReceiver) external onlyOwner { - selfdestruct(ethReceiver); - } + // solhint-enable /// @dev Signal support for receiving ERC1155 tokens. /// @param interfaceID The interface ID, as per ERC-165 rules. @@ -88,6 +110,7 @@ contract Puppet is interfaceID == this.onERC1155Received.selector ^ this.onERC1155BatchReceived.selector || interfaceID == this.tokenFallback.selector; } + /// @dev Allow this contract to receive ERC1155 tokens. /// @return success `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` function onERC1155Received( diff --git a/contracts/zero-ex/contracts/src/features/IPuppetPool.sol b/contracts/zero-ex/contracts/src/features/IPuppetPool.sol deleted file mode 100644 index 400f181d47..0000000000 --- a/contracts/zero-ex/contracts/src/features/IPuppetPool.sol +++ /dev/null @@ -1,55 +0,0 @@ -/* - - 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 "../puppets/IPuppet.sol"; - - -/// @dev Feature to manage a pool of puppet workers. -interface IPuppetPool { - - /// @dev A new puppet contract was created. - /// @param puppet The address of the puppet contract. - event PuppetCreated(address puppet); - - /// @dev Create a new, free puppet to add to the pool. Anyone can call this. - /// @return puppet The new puppet's address. - function createFreePuppet() external returns (address puppet); - - /// @dev Acquire a new puppet instance. This removes the puppet from the - /// pool. If one is not available, a new one will be deployed. - /// Only callable from within. - /// @return puppet The acquired puppet. - function _acquirePuppet() external returns (IPuppet puppet); - - /// @dev Release an acquired puppet instance back into the pool. - /// Only callable from within. - /// @param puppet The puppet to return to the pool. - function _releasePuppet(IPuppet puppet) external; - - /// @dev Gets the number of free puppets in the pool. - /// @return count The number of free puppets in the pool. - function getFreePuppetsCount() external view returns (uint256 count); - - /// @dev Check if an address is a puppet instance. - /// @param puppet The address to check. - /// @return isPuppet_ `true` if `puppet` is a puppet instance. - function isPuppet(address puppet) external view returns (bool isPuppet_); -} diff --git a/contracts/zero-ex/contracts/src/features/PuppetPool.sol b/contracts/zero-ex/contracts/src/features/PuppetPool.sol deleted file mode 100644 index b82945d773..0000000000 --- a/contracts/zero-ex/contracts/src/features/PuppetPool.sol +++ /dev/null @@ -1,169 +0,0 @@ -/* - - 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/errors/LibRichErrorsV06.sol"; -import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; -import "../errors/LibPuppetRichErrors.sol"; -import "../fixins/FixinCommon.sol"; -import "../migrations/LibMigrate.sol"; -import "../puppets/IPuppet.sol"; -import "../puppets/Puppet.sol"; -import "../storage/LibPuppetPoolStorage.sol"; -import "./ISimpleFunctionRegistry.sol"; -import "./IPuppetPool.sol"; -import "./IFeature.sol"; - - -/// @dev Feature to manage a pool of puppet workers. -contract PuppetPool is - IFeature, - IPuppetPool, - FixinCommon -{ - // solhint-disable const-name-snakecase - /// @dev Name of this feature. - string constant public override FEATURE_NAME = "PuppetPool"; - /// @dev Version of this feature. - uint256 constant public override FEATURE_VERSION = (1 << 64) | (0 << 32) | (0); - // solhint-enable const-name-snakecase - - /// @dev The implementation address of this feature. - address private immutable _impl; - - using LibSafeMathV06 for uint256; - using LibRichErrorsV06 for bytes; - - constructor() public { - _impl = address(this); - } - - /// @dev Initialize and register this feature. Should be delegatecalled - /// into during a `Migrate.migrate()`. - function migrate() external returns (bytes4 success) { - // Register this feature's functions. - ISimpleFunctionRegistry(address(this)) - .extend(this.createFreePuppet.selector, _impl); - ISimpleFunctionRegistry(address(this)) - .extend(this._acquirePuppet.selector, _impl); - ISimpleFunctionRegistry(address(this)) - .extend(this._releasePuppet.selector, _impl); - ISimpleFunctionRegistry(address(this)) - .extend(this.getFreePuppetsCount.selector, _impl); - ISimpleFunctionRegistry(address(this)) - .extend(this.isPuppet.selector, _impl); - return LibMigrate.MIGRATE_SUCCESS; - } - - /// @dev Create a new, free puppet to add to the pool. Anyone can call this. - /// @return puppet The new puppet's address. - function createFreePuppet() - external - override - returns (address puppet) - { - return address(_createPuppet(LibPuppetPoolStorage.PuppetState.Free)); - } - - /// @dev Acquire a new puppet instance. This removes the puppet from the - /// pool. If one is not available, a new one will be deployed. - /// Only callable from within. - /// @return puppet The acquired puppet. - function _acquirePuppet() - external - override - onlySelf - returns (IPuppet puppet) - { - LibPuppetPoolStorage.Storage storage stor = LibPuppetPoolStorage.getStorage(); - uint256 numFreePuppets = stor.freePuppets.length; - if (numFreePuppets == 0) { - puppet = _createPuppet(LibPuppetPoolStorage.PuppetState.Acquired); - } else { - puppet = stor.freePuppets[numFreePuppets - 1]; - stor.puppetState[address(puppet)] = LibPuppetPoolStorage.PuppetState.Acquired; - stor.freePuppets.pop(); - } - } - - /// @dev Release an acquired puppet instance back into the pool. - /// Only callable from within. - /// @param puppet The puppet to return to the pool. - function _releasePuppet(IPuppet puppet) - external - override - onlySelf - { - LibPuppetPoolStorage.Storage storage stor = LibPuppetPoolStorage.getStorage(); - // Validate puppet state. - LibPuppetPoolStorage.PuppetState state = stor.puppetState[address(puppet)]; - if (state == LibPuppetPoolStorage.PuppetState.Invalid) { - LibPuppetRichErrors.InvalidPuppetInstanceError(address(puppet)).rrevert(); - } else if (state == LibPuppetPoolStorage.PuppetState.Free) { - LibPuppetRichErrors.PuppetNotAcquiredError(address(puppet)).rrevert(); - } - // Return the puppet to the pool. - stor.puppetState[address(puppet)] = LibPuppetPoolStorage.PuppetState.Free; - stor.freePuppets.push(Puppet(address(uint160(address(puppet))))); - } - - /// @dev Gets the number of free puppets in the pool. - /// @return count The number of free puppets in the pool. - function getFreePuppetsCount() - external - override - view - returns (uint256 count) - { - return LibPuppetPoolStorage.getStorage().freePuppets.length; - } - - /// @dev Check if an address is a puppet instance. - /// @param puppet The address to check. - /// @return isPuppet_ `true` if `puppet` is a puppet instance. - function isPuppet(address puppet) - external - override - view - returns (bool isPuppet_) - { - LibPuppetPoolStorage.PuppetState state = - LibPuppetPoolStorage.getStorage().puppetState[address(puppet)]; - return state != LibPuppetPoolStorage.PuppetState.Invalid; - } - - /// @dev Deploy a new puppet instance with the provided state. - /// If `state` is `Free`, this will also add it to the free puppets pool. - /// @param state The state of the puppet. - /// @return puppet The new puppet instance. - function _createPuppet(LibPuppetPoolStorage.PuppetState state) - private - returns (Puppet puppet) - { - LibPuppetPoolStorage.Storage storage stor = LibPuppetPoolStorage.getStorage(); - puppet = new Puppet(); - puppet.addAuthorizedAddress(address(this)); - stor.puppetState[address(puppet)] = state; - if (state == LibPuppetPoolStorage.PuppetState.Free) { - stor.freePuppets.push(puppet); - } - emit PuppetCreated(address(puppet)); - } -} diff --git a/contracts/zero-ex/contracts/src/puppets/ITokenSpenderPuppet.sol b/contracts/zero-ex/contracts/src/puppets/ITokenSpenderPuppet.sol deleted file mode 100644 index 9982bc4b63..0000000000 --- a/contracts/zero-ex/contracts/src/puppets/ITokenSpenderPuppet.sol +++ /dev/null @@ -1,30 +0,0 @@ -/* - - 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 "./IPuppet.sol"; - - -/// @dev An alias for IPuppet, to differentiate from puppets used by PuppetPool. -interface ITokenSpenderPuppet is - IPuppet -{ - // solhint-disable no-empty-blocks -} diff --git a/contracts/zero-ex/contracts/src/puppets/TokenSpenderPuppet.sol b/contracts/zero-ex/contracts/src/puppets/TokenSpenderPuppet.sol deleted file mode 100644 index 2858fdc2bb..0000000000 --- a/contracts/zero-ex/contracts/src/puppets/TokenSpenderPuppet.sol +++ /dev/null @@ -1,33 +0,0 @@ -/* - - 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 "./ITokenSpenderPuppet.sol"; -import "./Puppet.sol"; - - -/// @dev A specialized puppet for use exclusively by the TokenSpender. -/// Essentially an alias to differentiate from puppets in PuppetPool. -contract TokenSpenderPuppet is - ITokenSpenderPuppet, - Puppet -{ - // solhint-disable no-empty-blocks -} diff --git a/contracts/zero-ex/contracts/src/storage/LibPuppetPoolStorage.sol b/contracts/zero-ex/contracts/src/storage/LibPuppetPoolStorage.sol deleted file mode 100644 index e3931863fd..0000000000 --- a/contracts/zero-ex/contracts/src/storage/LibPuppetPoolStorage.sol +++ /dev/null @@ -1,54 +0,0 @@ -/* - - 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 "./LibStorage.sol"; -import "../puppets/Puppet.sol"; - - -/// @dev Storage helpers for the `PuppetPool` feature. -library LibPuppetPoolStorage { - - /// @dev The state of a puppet instance. - enum PuppetState { - // Not a valid puppet (default) - Invalid, - // Puppet is free to be acquired. - Free, - // Puppet is currently acquired. - Acquired - } - - /// @dev Storage bucket for this feature. - struct Storage { - // State of a puppet instance. - mapping(address => PuppetState) puppetState; - // Free puppet instances. - Puppet[] freePuppets; - } - - /// @dev Get the storage bucket for this contract. - function getStorage() internal pure returns (Storage storage stor) { - uint256 storageSlot = LibStorage.getStorageSlot( - LibStorage.StorageId.PuppetPool - ); - assembly { stor_slot := storageSlot } - } -} diff --git a/contracts/zero-ex/contracts/test/TestPuppetPool.sol b/contracts/zero-ex/contracts/test/TestPuppetPool.sol deleted file mode 100644 index 9390a205a5..0000000000 --- a/contracts/zero-ex/contracts/test/TestPuppetPool.sol +++ /dev/null @@ -1,31 +0,0 @@ -/* - - 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/features/PuppetPool.sol"; - - -contract TestPuppetPool is - PuppetPool -{ - modifier onlySelf() override { - _; - } -} diff --git a/contracts/zero-ex/contracts/test/TestPuppetTarget.sol b/contracts/zero-ex/contracts/test/TestPuppetTarget.sol index 66b70d04b9..85b4164fd9 100644 --- a/contracts/zero-ex/contracts/test/TestPuppetTarget.sol +++ b/contracts/zero-ex/contracts/test/TestPuppetTarget.sol @@ -23,6 +23,7 @@ pragma experimental ABIEncoderV2; contract TestPuppetTarget { event PuppetTargetCalled( + address context, address sender, bytes data, uint256 value @@ -36,6 +37,7 @@ contract TestPuppetTarget { revert("TestPuppetTarget/REVERT"); } emit PuppetTargetCalled( + address(this), msg.sender, msg.data, msg.value diff --git a/contracts/zero-ex/test/features/puppet_pool_test.ts b/contracts/zero-ex/test/features/puppet_pool_test.ts deleted file mode 100644 index 13a245e8f9..0000000000 --- a/contracts/zero-ex/test/features/puppet_pool_test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { blockchainTests, constants, expect, verifyEventsFromLogs } from '@0x/contracts-test-utils'; -import { ZeroExRevertErrors } from '@0x/utils'; - -import { artifacts } from '../artifacts'; -import { abis } from '../utils/abis'; -import { fullMigrateAsync } from '../utils/migration'; -import { IPuppetPoolEvents, PuppetContract, PuppetPoolContract, ZeroExContract } from '../wrappers'; - -blockchainTests.resets('PuppetPool feature', env => { - let zeroEx: ZeroExContract; - let feature: PuppetPoolContract; - let unmanagedPuppet: PuppetContract; - - before(async () => { - const [owner] = await env.getAccountAddressesAsync(); - zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, { - puppetPool: (await PuppetContract.deployFrom0xArtifactAsync( - artifacts.TestPuppetPool, - env.provider, - env.txDefaults, - artifacts, - )).address, - }); - feature = new PuppetPoolContract(zeroEx.address, env.provider, env.txDefaults, abis); - unmanagedPuppet = await PuppetContract.deployFrom0xArtifactAsync( - artifacts.Puppet, - env.provider, - env.txDefaults, - artifacts, - ); - }); - - async function acquirePuppetAsync(): Promise { - const puppet = new PuppetContract( - await feature._acquirePuppet().callAsync(), - env.provider, - env.txDefaults, - abis, - ); - await feature._acquirePuppet().awaitTransactionSuccessAsync(); - return puppet; - } - - async function releasePuppetAsync(puppet: string | PuppetContract): Promise { - await feature - ._releasePuppet(typeof puppet === 'string' ? puppet : puppet.address) - .awaitTransactionSuccessAsync(); - } - - describe('_acquirePuppet() and _releasePuppet()', () => { - it('_acquirePuppet() creates a new puppet if none are available', async () => { - const acquiredPuppets = []; - while ((await feature.getFreePuppetsCount().callAsync()).gt(0)) { - acquiredPuppets.push(await acquirePuppetAsync()); - } - const puppetAddress = await feature._acquirePuppet().callAsync(); - const receipt = await feature._acquirePuppet().awaitTransactionSuccessAsync(); - expect(puppetAddress).to.not.eq(constants.NULL_ADDRESS); - verifyEventsFromLogs(receipt.logs, [{ puppet: puppetAddress }], IPuppetPoolEvents.PuppetCreated); - }); - - it('_acquirePuppet() returns a free puppet if available', async () => { - const freePuppetAddess = await feature.createFreePuppet().callAsync(); - await feature.createFreePuppet().awaitTransactionSuccessAsync(); - // Acquire the free puppet. - const puppetAddress = await feature._acquirePuppet().callAsync(); - const receipt = await feature._acquirePuppet().awaitTransactionSuccessAsync(); - expect(await feature.getFreePuppetsCount().callAsync()).to.bignumber.eq(0); - expect(puppetAddress).to.eq(freePuppetAddess); - verifyEventsFromLogs(receipt.logs, [], IPuppetPoolEvents.PuppetCreated); - }); - - it('can release an EXISTING puppet returned by _acquirePuppet()', async () => { - const freePuppetAddess = await feature.createFreePuppet().callAsync(); - await feature.createFreePuppet().awaitTransactionSuccessAsync(); - // Acquire the free puppet. - const puppetAddress = await feature._acquirePuppet().callAsync(); - await feature._acquirePuppet().awaitTransactionSuccessAsync(); - expect(await feature.getFreePuppetsCount().callAsync()).to.bignumber.eq(0); - expect(puppetAddress).to.eq(freePuppetAddess); - await releasePuppetAsync(puppetAddress); - expect(await feature.getFreePuppetsCount().callAsync()).to.bignumber.eq(1); - }); - - it('can acquire and release many puppets', async () => { - const puppets = []; - for (let i = 0; i < 8; ++i) { - puppets.push(await acquirePuppetAsync()); - } - expect(await feature.getFreePuppetsCount().callAsync()).to.bignumber.eq(0); - for (const puppet of puppets) { - await releasePuppetAsync(puppet); - } - expect(await feature.getFreePuppetsCount().callAsync()).to.bignumber.eq(puppets.length); - }); - - it('cannot release a puppet not created by the pool', async () => { - return expect(releasePuppetAsync(unmanagedPuppet)).to.revertWith( - new ZeroExRevertErrors.Puppet.InvalidPuppetInstanceError(unmanagedPuppet.address), - ); - }); - - it('cannot release a free puppet', async () => { - const puppet = await acquirePuppetAsync(); - await releasePuppetAsync(puppet); - return expect(releasePuppetAsync(puppet)).to.revertWith( - new ZeroExRevertErrors.Puppet.PuppetNotAcquiredError(puppet.address), - ); - }); - }); - - describe('createFreePuppet()', () => { - it('creates a free puppet', async () => { - const puppet = await feature.createFreePuppet().callAsync(); - const receipt = await feature.createFreePuppet().awaitTransactionSuccessAsync(); - verifyEventsFromLogs(receipt.logs, [{ puppet }], IPuppetPoolEvents.PuppetCreated); - expect(await feature.isPuppet(puppet).callAsync()).to.eq(true); - expect(await feature.getFreePuppetsCount().callAsync()).to.bignumber.eq(1); - }); - }); - - describe('isPuppet()', () => { - it('returns false for a puppet not created by the pool', async () => { - expect(await feature.isPuppet(unmanagedPuppet.address).callAsync()).to.eq(false); - }); - - it('returns true for an acquired puppet', async () => { - const puppet = await acquirePuppetAsync(); - expect(await feature.isPuppet(puppet.address).callAsync()).to.eq(true); - }); - - it('returns true for a released puppet', async () => { - const puppet = await acquirePuppetAsync(); - await releasePuppetAsync(puppet); - expect(await feature.isPuppet(puppet.address).callAsync()).to.eq(true); - }); - }); - - describe('puppets', () => { - it('puppet is owned by proxy contract', async () => { - const puppet = await acquirePuppetAsync(); - expect(await puppet.owner().callAsync()).to.eq(zeroEx.address); - }); - - it('proxy contract is authorized', async () => { - const puppet = await acquirePuppetAsync(); - expect(await puppet.authorized(zeroEx.address).callAsync()).to.eq(true); - }); - }); -}); diff --git a/contracts/zero-ex/test/puppet_test.ts b/contracts/zero-ex/test/puppet_test.ts index 190395b18a..6514c636fa 100644 --- a/contracts/zero-ex/test/puppet_test.ts +++ b/contracts/zero-ex/test/puppet_test.ts @@ -6,26 +6,27 @@ import { randomAddress, verifyEventsFromLogs, } from '@0x/contracts-test-utils'; -import { AuthorizableRevertErrors, hexUtils, StringRevertError, ZeroExRevertErrors } from '@0x/utils'; +import { hexUtils, OwnableRevertErrors, StringRevertError, ZeroExRevertErrors } from '@0x/utils'; import { artifacts } from './artifacts'; import { PuppetContract, TestPuppetTargetContract, TestPuppetTargetEvents } from './wrappers'; blockchainTests.resets('Puppets', env => { let owner: string; - let authority: string; let puppet: PuppetContract; let puppetTarget: TestPuppetTargetContract; before(async () => { - [owner, authority] = await env.getAccountAddressesAsync(); + [owner] = await env.getAccountAddressesAsync(); puppet = await PuppetContract.deployFrom0xArtifactAsync( artifacts.Puppet, env.provider, - env.txDefaults, + { + ...env.txDefaults, + from: owner, + }, artifacts, ); - await puppet.addAuthorizedAddress(authority).awaitTransactionSuccessAsync(); puppetTarget = await TestPuppetTargetContract.deployFrom0xArtifactAsync( artifacts.TestPuppetTarget, env.provider, @@ -34,84 +35,177 @@ blockchainTests.resets('Puppets', env => { ); }); - it('non-authority cannot call execute()', async () => { - const notAuthority = randomAddress(); - const tx = puppet - .execute(randomAddress(), hexUtils.random(), getRandomInteger(0, '100e18')) - .callAsync({ from: notAuthority }); - return expect(tx).to.revertWith(new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthority)); - }); + const TARGET_RETURN_VALUE = hexUtils.rightPad('0x12345678'); + const REVERTING_DATA = '0x1337'; - it('authority can call execute()', async () => { - const targetData = hexUtils.random(128); - const receipt = await puppet - .execute(puppetTarget.address, targetData, constants.ZERO_AMOUNT) - .awaitTransactionSuccessAsync({ from: authority }); - verifyEventsFromLogs( - receipt.logs, - [ - { - sender: puppet.address, - data: targetData, - value: constants.ZERO_AMOUNT, - }, - ], - TestPuppetTargetEvents.PuppetTargetCalled, - ); + it('owned by deployer', () => { + return expect(puppet.owner().callAsync()).to.eventually.eq(owner); }); - it('authority can call execute() with attached ETH', async () => { - const targetData = hexUtils.random(128); - const callValue = getRandomInteger(1, '1e18'); - const receipt = await puppet - .execute(puppetTarget.address, targetData, callValue) - .awaitTransactionSuccessAsync({ from: authority, value: callValue }); - verifyEventsFromLogs( - receipt.logs, - [ - { - sender: puppet.address, - data: targetData, - value: callValue, - }, - ], - TestPuppetTargetEvents.PuppetTargetCalled, - ); - }); + describe('execute()', () => { + it('non-owner cannot call execute()', async () => { + const notOwner = randomAddress(); + const tx = puppet + .execute(randomAddress(), hexUtils.random(), getRandomInteger(0, '100e18')) + .callAsync({ from: notOwner }); + return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner)); + }); - const TARGET_RETURN_VALUE = hexUtils.rightPad('0x12345678'); + it('owner can call execute()', async () => { + const targetData = hexUtils.random(128); + const receipt = await puppet + .execute(puppetTarget.address, targetData, constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: owner }); + verifyEventsFromLogs( + receipt.logs, + [ + { + context: puppetTarget.address, + sender: puppet.address, + data: targetData, + value: constants.ZERO_AMOUNT, + }, + ], + TestPuppetTargetEvents.PuppetTargetCalled, + ); + }); - it('puppet returns call result', async () => { - const result = await puppet - .execute(puppetTarget.address, hexUtils.random(128), constants.ZERO_AMOUNT) - .callAsync({ from: authority }); - expect(result).to.eq(TARGET_RETURN_VALUE); - }); + it('owner can call execute() with attached ETH', async () => { + const targetData = hexUtils.random(128); + const callValue = getRandomInteger(1, '1e18'); + const receipt = await puppet + .execute(puppetTarget.address, targetData, callValue) + .awaitTransactionSuccessAsync({ from: owner, value: callValue }); + verifyEventsFromLogs( + receipt.logs, + [ + { + context: puppetTarget.address, + sender: puppet.address, + data: targetData, + value: callValue, + }, + ], + TestPuppetTargetEvents.PuppetTargetCalled, + ); + }); - const REVERTING_DATA = '0x1337'; + it('owner can call execute() can transfer less ETH than attached', async () => { + const targetData = hexUtils.random(128); + const callValue = getRandomInteger(1, '1e18'); + const receipt = await puppet + .execute(puppetTarget.address, targetData, callValue.minus(1)) + .awaitTransactionSuccessAsync({ from: owner, value: callValue }); + verifyEventsFromLogs( + receipt.logs, + [ + { + context: puppetTarget.address, + sender: puppet.address, + data: targetData, + value: callValue.minus(1), + }, + ], + TestPuppetTargetEvents.PuppetTargetCalled, + ); + }); - it('puppet wraps call revert', async () => { - const tx = puppet - .execute(puppetTarget.address, REVERTING_DATA, constants.ZERO_AMOUNT) - .callAsync({ from: authority }); - return expect(tx).to.revertWith( - new ZeroExRevertErrors.Puppet.PuppetExecuteFailedError( - puppet.address, - puppetTarget.address, - REVERTING_DATA, - constants.ZERO_AMOUNT, - new StringRevertError('TestPuppetTarget/REVERT').encode(), - ), - ); + it('puppet returns call result', async () => { + const result = await puppet + .execute(puppetTarget.address, hexUtils.random(128), constants.ZERO_AMOUNT) + .callAsync({ from: owner }); + expect(result).to.eq(TARGET_RETURN_VALUE); + }); + + it('puppet wraps call revert', async () => { + const tx = puppet + .execute(puppetTarget.address, REVERTING_DATA, constants.ZERO_AMOUNT) + .callAsync({ from: owner }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.Puppet.PuppetExecuteFailedError( + puppet.address, + puppetTarget.address, + REVERTING_DATA, + constants.ZERO_AMOUNT, + new StringRevertError('TestPuppetTarget/REVERT').encode(), + ), + ); + }); + + it('puppet can receive ETH', async () => { + await env.web3Wrapper.sendTransactionAsync({ + to: puppet.address, + from: owner, + value: 1, + }); + const bal = await env.web3Wrapper.getBalanceInWeiAsync(puppet.address); + expect(bal).to.bignumber.eq(1); + }); }); - it('puppet can receive ETH', async () => { - await env.web3Wrapper.sendTransactionAsync({ - to: puppet.address, - from: owner, - value: 1, + describe('executeWith()', () => { + it('non-owner cannot call executeWith()', async () => { + const notOwner = randomAddress(); + const tx = puppet.executeWith(randomAddress(), hexUtils.random()).callAsync({ from: notOwner }); + return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner)); + }); + + it('owner can call executeWith()', async () => { + const targetData = hexUtils.random(128); + const receipt = await puppet + .executeWith(puppetTarget.address, targetData) + .awaitTransactionSuccessAsync({ from: owner }); + verifyEventsFromLogs( + receipt.logs, + [ + { + context: puppet.address, + sender: owner, + data: targetData, + value: constants.ZERO_AMOUNT, + }, + ], + TestPuppetTargetEvents.PuppetTargetCalled, + ); + }); + + it('executeWith() is payable', async () => { + const targetData = hexUtils.random(128); + const callValue = getRandomInteger(1, '1e18'); + const receipt = await puppet + .executeWith(puppetTarget.address, targetData) + .awaitTransactionSuccessAsync({ from: owner, value: callValue }); + verifyEventsFromLogs( + receipt.logs, + [ + { + context: puppet.address, + sender: owner, + data: targetData, + value: callValue, + }, + ], + TestPuppetTargetEvents.PuppetTargetCalled, + ); + }); + + it('puppet returns call result', async () => { + const result = await puppet + .executeWith(puppetTarget.address, hexUtils.random(128)) + .callAsync({ from: owner }); + expect(result).to.eq(TARGET_RETURN_VALUE); + }); + + it('puppet wraps call revert', async () => { + const tx = puppet.executeWith(puppetTarget.address, REVERTING_DATA).callAsync({ from: owner }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.Puppet.PuppetExecuteWithFailedError( + puppet.address, + puppetTarget.address, + REVERTING_DATA, + new StringRevertError('TestPuppetTarget/REVERT').encode(), + ), + ); }); - const bal = await env.web3Wrapper.getBalanceInWeiAsync(puppet.address); - expect(bal).to.bignumber.eq(1); }); }); From 030cb285da4dbd6e59346f7d496d8f4296357245 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 15 May 2020 01:21:24 -0400 Subject: [PATCH 07/19] `@0x/contracts-zero-ex`: Use `immutable` owner in `Puppet` instead of `Ownable`. --- .../contracts/src/external/IPuppet.sol | 9 ++++--- .../zero-ex/contracts/src/external/Puppet.sol | 26 ++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/contracts/zero-ex/contracts/src/external/IPuppet.sol b/contracts/zero-ex/contracts/src/external/IPuppet.sol index e17725b0ee..f349476db9 100644 --- a/contracts/zero-ex/contracts/src/external/IPuppet.sol +++ b/contracts/zero-ex/contracts/src/external/IPuppet.sol @@ -23,9 +23,8 @@ import "@0x/contracts-utils/contracts/src/v06/interfaces/IOwnableV06.sol"; /// @dev A contract that can execute arbitrary calls from its owner. -interface IPuppet is - IOwnableV06 -{ +interface IPuppet { + /// @dev Execute an arbitrary call. Only an authority can call this. /// @param target The call target. /// @param callData The call data. @@ -55,4 +54,8 @@ interface IPuppet is /// @dev Allows the puppet to receive ETH. receive() external payable; + + /// @dev Fetch the immutable owner/deployer of this contract. + /// @return owner_ The immutable owner/deployer/ + function owner() external view returns (address owner_); } diff --git a/contracts/zero-ex/contracts/src/external/Puppet.sol b/contracts/zero-ex/contracts/src/external/Puppet.sol index 2d1d700c2f..aea8457bde 100644 --- a/contracts/zero-ex/contracts/src/external/Puppet.sol +++ b/contracts/zero-ex/contracts/src/external/Puppet.sol @@ -20,19 +20,39 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; -import "@0x/contracts-utils/contracts/src/v06/OwnableV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/errors/LibOwnableRichErrorsV06.sol"; import "../errors/LibPuppetRichErrors.sol"; import "./IPuppet.sol"; /// @dev A contract that can execute arbitrary calls from its owner. contract Puppet is - IPuppet, - OwnableV06 + IPuppet { // solhint-disable no-unused-vars,indent,no-empty-blocks using LibRichErrorsV06 for bytes; + // solhint-disable + /// @dev Store the owner/deployer as an immutable to make this contract stateless. + address public override immutable owner; + // solhint-enable + + constructor() public { + // The deployer is the owner. + owner = msg.sender; + } + + /// @dev Allows only the (immutable) owner to call a function. + modifier onlyOwner() virtual { + if (msg.sender != owner) { + LibOwnableRichErrorsV06.OnlyOwnerError( + msg.sender, + owner + ).rrevert(); + } + _; + } + /// @dev Execute an arbitrary call. Only an authority can call this. /// @param target The call target. /// @param callData The call data. From d2f581853dbcf6f1f5d6c37d5f9be0e8bbb9f72a Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 18 May 2020 14:55:43 -0400 Subject: [PATCH 08/19] `@x/utils`: Address review feedback. --- .../zero-ex/puppet_revert_errors.ts | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 packages/utils/src/revert_errors/zero-ex/puppet_revert_errors.ts diff --git a/packages/utils/src/revert_errors/zero-ex/puppet_revert_errors.ts b/packages/utils/src/revert_errors/zero-ex/puppet_revert_errors.ts deleted file mode 100644 index 8885b39612..0000000000 --- a/packages/utils/src/revert_errors/zero-ex/puppet_revert_errors.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { RevertError } from '../../revert_error'; -import { Numberish } from '../../types'; - -// tslint:disable:max-classes-per-file -export class PuppetExecuteFailedError extends RevertError { - constructor(puppet?: string, callTarget?: string, callData?: string, callValue?: Numberish, errorData?: string) { - super( - 'PuppetExecuteFailedError', - 'PuppetExecuteFailedError(address puppet, address callTarget, bytes callData, uint256 callValue, bytes errorData)', - { - puppet, - callTarget, - callData, - callValue, - errorData, - }, - ); - } -} - -export class PuppetExecuteWithFailedError extends RevertError { - constructor(puppet?: string, callTarget?: string, callData?: string, errorData?: string) { - super( - 'PuppetExecuteWithFailedError', - 'PuppetExecuteWithFailedError(address puppet, address callTarget, bytes callData, bytes errorData)', - { - puppet, - callTarget, - callData, - errorData, - }, - ); - } -} - -const types = [PuppetExecuteFailedError, PuppetExecuteWithFailedError]; - -// Register the types we've defined. -for (const type of types) { - RevertError.registerType(type); -} From 6359f1950eaf42dc4cda0f9fb549ab7418a75d29 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 18 May 2020 14:55:57 -0400 Subject: [PATCH 09/19] `@0x/contracts-zero-ex`: Address review feedback. --- .../src/errors/LibPuppetRichErrors.sol | 65 ------ .../contracts/src/external/IPuppet.sol | 61 ----- .../zero-ex/contracts/src/external/Puppet.sol | 175 --------------- .../contracts/test/TestPuppetTarget.sol | 51 ----- contracts/zero-ex/test/puppet_test.ts | 211 ------------------ 5 files changed, 563 deletions(-) delete mode 100644 contracts/zero-ex/contracts/src/errors/LibPuppetRichErrors.sol delete mode 100644 contracts/zero-ex/contracts/src/external/IPuppet.sol delete mode 100644 contracts/zero-ex/contracts/src/external/Puppet.sol delete mode 100644 contracts/zero-ex/contracts/test/TestPuppetTarget.sol delete mode 100644 contracts/zero-ex/test/puppet_test.ts diff --git a/contracts/zero-ex/contracts/src/errors/LibPuppetRichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibPuppetRichErrors.sol deleted file mode 100644 index 5d7e3ff043..0000000000 --- a/contracts/zero-ex/contracts/src/errors/LibPuppetRichErrors.sol +++ /dev/null @@ -1,65 +0,0 @@ -/* - - 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; - - -library LibPuppetRichErrors { - - // solhint-disable func-name-mixedcase - - function PuppetExecuteFailedError( - address puppet, - address callTarget, - bytes memory callData, - uint256 callValue, - bytes memory errorData - ) - internal - pure - returns (bytes memory) - { - return abi.encodeWithSelector( - bytes4(keccak256("PuppetExecuteFailedError(address,address,bytes,uint256,bytes)")), - puppet, - callTarget, - callData, - callValue, - errorData - ); - } - - function PuppetExecuteWithFailedError( - address puppet, - address callTarget, - bytes memory callData, - bytes memory errorData - ) - internal - pure - returns (bytes memory) - { - return abi.encodeWithSelector( - bytes4(keccak256("PuppetExecuteWithFailedError(address,address,bytes,bytes)")), - puppet, - callTarget, - callData, - errorData - ); - } -} diff --git a/contracts/zero-ex/contracts/src/external/IPuppet.sol b/contracts/zero-ex/contracts/src/external/IPuppet.sol deleted file mode 100644 index f349476db9..0000000000 --- a/contracts/zero-ex/contracts/src/external/IPuppet.sol +++ /dev/null @@ -1,61 +0,0 @@ -/* - - 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/interfaces/IOwnableV06.sol"; - - -/// @dev A contract that can execute arbitrary calls from its owner. -interface IPuppet { - - /// @dev Execute an arbitrary call. Only an authority can call this. - /// @param target The call target. - /// @param callData The call data. - /// @param value Ether to attach to the call. - /// @return resultData The data returned by the call. - function execute( - address payable target, - bytes calldata callData, - uint256 value - ) - external - payable - returns (bytes memory resultData); - - /// @dev Execute an arbitrary delegatecall, in the context of this puppet. - /// Only an authority can call this. - /// @param target The call target. - /// @param callData The call data. - /// @return resultData The data returned by the call. - function executeWith( - address payable target, - bytes calldata callData - ) - external - payable - returns (bytes memory resultData); - - /// @dev Allows the puppet to receive ETH. - receive() external payable; - - /// @dev Fetch the immutable owner/deployer of this contract. - /// @return owner_ The immutable owner/deployer/ - function owner() external view returns (address owner_); -} diff --git a/contracts/zero-ex/contracts/src/external/Puppet.sol b/contracts/zero-ex/contracts/src/external/Puppet.sol deleted file mode 100644 index aea8457bde..0000000000 --- a/contracts/zero-ex/contracts/src/external/Puppet.sol +++ /dev/null @@ -1,175 +0,0 @@ -/* - - 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/errors/LibRichErrorsV06.sol"; -import "@0x/contracts-utils/contracts/src/v06/errors/LibOwnableRichErrorsV06.sol"; -import "../errors/LibPuppetRichErrors.sol"; -import "./IPuppet.sol"; - - -/// @dev A contract that can execute arbitrary calls from its owner. -contract Puppet is - IPuppet -{ - // solhint-disable no-unused-vars,indent,no-empty-blocks - using LibRichErrorsV06 for bytes; - - // solhint-disable - /// @dev Store the owner/deployer as an immutable to make this contract stateless. - address public override immutable owner; - // solhint-enable - - constructor() public { - // The deployer is the owner. - owner = msg.sender; - } - - /// @dev Allows only the (immutable) owner to call a function. - modifier onlyOwner() virtual { - if (msg.sender != owner) { - LibOwnableRichErrorsV06.OnlyOwnerError( - msg.sender, - owner - ).rrevert(); - } - _; - } - - /// @dev Execute an arbitrary call. Only an authority can call this. - /// @param target The call target. - /// @param callData The call data. - /// @param value Ether to attach to the call. - /// @return resultData The data returned by the call. - function execute( - address payable target, - bytes calldata callData, - uint256 value - ) - external - payable - override - onlyOwner - returns (bytes memory resultData) - { - bool success; - (success, resultData) = target.call{value: value}(callData); - if (!success) { - LibPuppetRichErrors - .PuppetExecuteFailedError( - address(this), - target, - callData, - value, - resultData - ) - .rrevert(); - } - } - - /// @dev Execute an arbitrary delegatecall, in the context of this puppet. - /// Only an authority can call this. - /// @param target The call target. - /// @param callData The call data. - /// @return resultData The data returned by the call. - function executeWith( - address payable target, - bytes calldata callData - ) - external - payable - override - onlyOwner - returns (bytes memory resultData) - { - bool success; - (success, resultData) = target.delegatecall(callData); - if (!success) { - LibPuppetRichErrors - .PuppetExecuteWithFailedError( - address(this), - target, - callData, - resultData - ) - .rrevert(); - } - } - - // solhint-disable - /// @dev Allows this contract to receive ether. - receive() external override payable {} - // solhint-enable - - /// @dev Signal support for receiving ERC1155 tokens. - /// @param interfaceID The interface ID, as per ERC-165 rules. - /// @return hasSupport `true` if this contract supports an ERC-165 interface. - function supportsInterface(bytes4 interfaceID) - external - pure - returns (bool hasSupport) - { - return interfaceID == this.supportsInterface.selector || - interfaceID == this.onERC1155Received.selector ^ this.onERC1155BatchReceived.selector || - interfaceID == this.tokenFallback.selector; - } - - /// @dev Allow this contract to receive ERC1155 tokens. - /// @return success `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` - function onERC1155Received( - address, // operator, - address, // from, - uint256, // id, - uint256, // value, - bytes calldata //data - ) - external - pure - returns (bytes4 success) - { - return this.onERC1155Received.selector; - } - - /// @dev Allow this contract to receive ERC1155 tokens. - /// @return success `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` - function onERC1155BatchReceived( - address, // operator, - address, // from, - uint256[] calldata, // ids, - uint256[] calldata, // values, - bytes calldata // data - ) - external - pure - returns (bytes4 success) - { - return this.onERC1155BatchReceived.selector; - } - - /// @dev Allows this contract to receive ERC223 tokens. - function tokenFallback( - address, // from, - uint256, // value, - bytes calldata // value - ) - external - pure - {} -} diff --git a/contracts/zero-ex/contracts/test/TestPuppetTarget.sol b/contracts/zero-ex/contracts/test/TestPuppetTarget.sol deleted file mode 100644 index 85b4164fd9..0000000000 --- a/contracts/zero-ex/contracts/test/TestPuppetTarget.sol +++ /dev/null @@ -1,51 +0,0 @@ -/* - - 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; - - -contract TestPuppetTarget { - - event PuppetTargetCalled( - address context, - address sender, - bytes data, - uint256 value - ); - - bytes4 private constant MAGIC_BYTES = 0x12345678; - bytes private constant REVERTING_DATA = hex"1337"; - - fallback() external payable { - if (keccak256(msg.data) == keccak256(REVERTING_DATA)) { - revert("TestPuppetTarget/REVERT"); - } - emit PuppetTargetCalled( - address(this), - msg.sender, - msg.data, - msg.value - ); - bytes4 rval = MAGIC_BYTES; - assembly { - mstore(0, rval) - return(0, 32) - } - } -} diff --git a/contracts/zero-ex/test/puppet_test.ts b/contracts/zero-ex/test/puppet_test.ts deleted file mode 100644 index 6514c636fa..0000000000 --- a/contracts/zero-ex/test/puppet_test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { - blockchainTests, - constants, - expect, - getRandomInteger, - randomAddress, - verifyEventsFromLogs, -} from '@0x/contracts-test-utils'; -import { hexUtils, OwnableRevertErrors, StringRevertError, ZeroExRevertErrors } from '@0x/utils'; - -import { artifacts } from './artifacts'; -import { PuppetContract, TestPuppetTargetContract, TestPuppetTargetEvents } from './wrappers'; - -blockchainTests.resets('Puppets', env => { - let owner: string; - let puppet: PuppetContract; - let puppetTarget: TestPuppetTargetContract; - - before(async () => { - [owner] = await env.getAccountAddressesAsync(); - puppet = await PuppetContract.deployFrom0xArtifactAsync( - artifacts.Puppet, - env.provider, - { - ...env.txDefaults, - from: owner, - }, - artifacts, - ); - puppetTarget = await TestPuppetTargetContract.deployFrom0xArtifactAsync( - artifacts.TestPuppetTarget, - env.provider, - env.txDefaults, - artifacts, - ); - }); - - const TARGET_RETURN_VALUE = hexUtils.rightPad('0x12345678'); - const REVERTING_DATA = '0x1337'; - - it('owned by deployer', () => { - return expect(puppet.owner().callAsync()).to.eventually.eq(owner); - }); - - describe('execute()', () => { - it('non-owner cannot call execute()', async () => { - const notOwner = randomAddress(); - const tx = puppet - .execute(randomAddress(), hexUtils.random(), getRandomInteger(0, '100e18')) - .callAsync({ from: notOwner }); - return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner)); - }); - - it('owner can call execute()', async () => { - const targetData = hexUtils.random(128); - const receipt = await puppet - .execute(puppetTarget.address, targetData, constants.ZERO_AMOUNT) - .awaitTransactionSuccessAsync({ from: owner }); - verifyEventsFromLogs( - receipt.logs, - [ - { - context: puppetTarget.address, - sender: puppet.address, - data: targetData, - value: constants.ZERO_AMOUNT, - }, - ], - TestPuppetTargetEvents.PuppetTargetCalled, - ); - }); - - it('owner can call execute() with attached ETH', async () => { - const targetData = hexUtils.random(128); - const callValue = getRandomInteger(1, '1e18'); - const receipt = await puppet - .execute(puppetTarget.address, targetData, callValue) - .awaitTransactionSuccessAsync({ from: owner, value: callValue }); - verifyEventsFromLogs( - receipt.logs, - [ - { - context: puppetTarget.address, - sender: puppet.address, - data: targetData, - value: callValue, - }, - ], - TestPuppetTargetEvents.PuppetTargetCalled, - ); - }); - - it('owner can call execute() can transfer less ETH than attached', async () => { - const targetData = hexUtils.random(128); - const callValue = getRandomInteger(1, '1e18'); - const receipt = await puppet - .execute(puppetTarget.address, targetData, callValue.minus(1)) - .awaitTransactionSuccessAsync({ from: owner, value: callValue }); - verifyEventsFromLogs( - receipt.logs, - [ - { - context: puppetTarget.address, - sender: puppet.address, - data: targetData, - value: callValue.minus(1), - }, - ], - TestPuppetTargetEvents.PuppetTargetCalled, - ); - }); - - it('puppet returns call result', async () => { - const result = await puppet - .execute(puppetTarget.address, hexUtils.random(128), constants.ZERO_AMOUNT) - .callAsync({ from: owner }); - expect(result).to.eq(TARGET_RETURN_VALUE); - }); - - it('puppet wraps call revert', async () => { - const tx = puppet - .execute(puppetTarget.address, REVERTING_DATA, constants.ZERO_AMOUNT) - .callAsync({ from: owner }); - return expect(tx).to.revertWith( - new ZeroExRevertErrors.Puppet.PuppetExecuteFailedError( - puppet.address, - puppetTarget.address, - REVERTING_DATA, - constants.ZERO_AMOUNT, - new StringRevertError('TestPuppetTarget/REVERT').encode(), - ), - ); - }); - - it('puppet can receive ETH', async () => { - await env.web3Wrapper.sendTransactionAsync({ - to: puppet.address, - from: owner, - value: 1, - }); - const bal = await env.web3Wrapper.getBalanceInWeiAsync(puppet.address); - expect(bal).to.bignumber.eq(1); - }); - }); - - describe('executeWith()', () => { - it('non-owner cannot call executeWith()', async () => { - const notOwner = randomAddress(); - const tx = puppet.executeWith(randomAddress(), hexUtils.random()).callAsync({ from: notOwner }); - return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner)); - }); - - it('owner can call executeWith()', async () => { - const targetData = hexUtils.random(128); - const receipt = await puppet - .executeWith(puppetTarget.address, targetData) - .awaitTransactionSuccessAsync({ from: owner }); - verifyEventsFromLogs( - receipt.logs, - [ - { - context: puppet.address, - sender: owner, - data: targetData, - value: constants.ZERO_AMOUNT, - }, - ], - TestPuppetTargetEvents.PuppetTargetCalled, - ); - }); - - it('executeWith() is payable', async () => { - const targetData = hexUtils.random(128); - const callValue = getRandomInteger(1, '1e18'); - const receipt = await puppet - .executeWith(puppetTarget.address, targetData) - .awaitTransactionSuccessAsync({ from: owner, value: callValue }); - verifyEventsFromLogs( - receipt.logs, - [ - { - context: puppet.address, - sender: owner, - data: targetData, - value: callValue, - }, - ], - TestPuppetTargetEvents.PuppetTargetCalled, - ); - }); - - it('puppet returns call result', async () => { - const result = await puppet - .executeWith(puppetTarget.address, hexUtils.random(128)) - .callAsync({ from: owner }); - expect(result).to.eq(TARGET_RETURN_VALUE); - }); - - it('puppet wraps call revert', async () => { - const tx = puppet.executeWith(puppetTarget.address, REVERTING_DATA).callAsync({ from: owner }); - return expect(tx).to.revertWith( - new ZeroExRevertErrors.Puppet.PuppetExecuteWithFailedError( - puppet.address, - puppetTarget.address, - REVERTING_DATA, - new StringRevertError('TestPuppetTarget/REVERT').encode(), - ), - ); - }); - }); -}); From cfc3daeb6590409ce44f7093361e0aba088b77dc Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 6 May 2020 17:09:46 -0400 Subject: [PATCH 10/19] `@0x/utils`: Add ERC20 transformer revert errors. --- packages/utils/CHANGELOG.json | 4 ++++ .../zero-ex/transform_erc20_revert_errors.ts | 22 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/utils/CHANGELOG.json b/packages/utils/CHANGELOG.json index ff55eb6f39..35151be73b 100644 --- a/packages/utils/CHANGELOG.json +++ b/packages/utils/CHANGELOG.json @@ -21,6 +21,10 @@ { "note": "Add more `ZeroExRevertErrors`", "pr": 2545 + }, + { + "note": "Add more `ZeroExRevertErrors`", + "pr": 2576 } ] }, diff --git a/packages/utils/src/revert_errors/zero-ex/transform_erc20_revert_errors.ts b/packages/utils/src/revert_errors/zero-ex/transform_erc20_revert_errors.ts index f8133bee3a..1502121369 100644 --- a/packages/utils/src/revert_errors/zero-ex/transform_erc20_revert_errors.ts +++ b/packages/utils/src/revert_errors/zero-ex/transform_erc20_revert_errors.ts @@ -53,11 +53,19 @@ export class InvalidRLPNonceError extends RevertError { } } +export class InvalidTransformDataError extends RevertError { + constructor(transformData?: string) { + super('InvalidTransformDataError', 'InvalidTransformDataError(bytes transformData)', { + transformData, + }); + } +} + export class IncompleteFillSellQuoteError extends RevertError { constructor(sellToken?: string, soldAmount?: Numberish, sellAmount?: Numberish) { super( 'IncompleteFillSellQuoteError', - 'IncompleteFillSellQuoteError(address sellToken, address[] soldAmount, uint256[] sellAmount)', + 'IncompleteFillSellQuoteError(address sellToken, uint256 soldAmount, uint256 sellAmount)', { sellToken, soldAmount, @@ -71,7 +79,7 @@ export class IncompleteFillBuyQuoteError extends RevertError { constructor(buyToken?: string, boughtAmount?: Numberish, buyAmount?: Numberish) { super( 'IncompleteFillBuyQuoteError', - 'IncompleteFillBuyQuoteError(address buyToken, address[] boughtAmount, uint256[] buyAmount)', + 'IncompleteFillBuyQuoteError(address buyToken, uint256 boughtAmount, uint256 buyAmount)', { buyToken, boughtAmount, @@ -132,6 +140,14 @@ export class InvalidTokenReceivedError extends RevertError { } } +export class InvalidTakerFeeTokenError extends RevertError { + constructor(token?: string) { + super('InvalidTakerFeeTokenError', 'InvalidTakerFeeTokenError(address token)', { + token, + }); + } +} + const types = [ InsufficientEthAttachedError, IncompleteTransformERC20Error, @@ -145,6 +161,8 @@ const types = [ InvalidERC20AssetDataError, WrongNumberOfTokensReceivedError, InvalidTokenReceivedError, + InvalidTransformDataError, + InvalidTakerFeeTokenError, ]; // Register the types we've defined. From 0e1a5a375a2fd2a65ba1f38a4a667a4133a0dd92 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 8 May 2020 12:25:29 -0400 Subject: [PATCH 11/19] `@0x/contracts-test-utils`: Add `msg` param to `assertIntegerRoughlyEquals` --- contracts/test-utils/CHANGELOG.json | 9 +++++++++ contracts/test-utils/src/number_utils.ts | 9 +++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index 4465213c1e..ec1484db70 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "5.3.3", + "changes": [ + { + "note": "Add `msg` param to `assertIntegerRoughlyEquals`", + "pr": 2576 + } + ] + }, { "timestamp": 1583220306, "version": "5.3.2", diff --git a/contracts/test-utils/src/number_utils.ts b/contracts/test-utils/src/number_utils.ts index eacd16a642..52df32cbb8 100644 --- a/contracts/test-utils/src/number_utils.ts +++ b/contracts/test-utils/src/number_utils.ts @@ -96,7 +96,12 @@ export function assertRoughlyEquals(actual: Numberish, expected: Numberish, prec /** * Asserts that two numbers are equal with up to `maxError` difference between them. */ -export function assertIntegerRoughlyEquals(actual: Numberish, expected: Numberish, maxError: number = 1): void { +export function assertIntegerRoughlyEquals( + actual: Numberish, + expected: Numberish, + maxError: number = 1, + msg?: string, +): void { const diff = new BigNumber(actual) .minus(expected) .abs() @@ -104,7 +109,7 @@ export function assertIntegerRoughlyEquals(actual: Numberish, expected: Numberis if (diff <= maxError) { return; } - expect(actual).to.bignumber.eq(expected); + expect(actual, msg).to.bignumber.eq(expected); } /** From 2ba3818b65ecc02ddd5ae0d5e283553d313a6f9e Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 9 Apr 2020 13:20:53 -0400 Subject: [PATCH 12/19] `@0x/contracts-zero-ex`: Introduce transformer contracts. --- .../errors/LibTransformERC20RichErrors.sol | 37 +- .../src/transformers/FillQuoteTransformer.sol | 418 +++++++++ .../src/transformers/PayTakerTransformer.sol | 77 ++ .../src/transformers/WethTransformer.sol | 91 ++ .../contracts/src/vendor/v3/IExchange.sol | 107 +++ .../test/TestFillQuoteTransformerExchange.sol | 145 +++ .../test/TestFillQuoteTransformerHost.sol | 45 + .../contracts/test/TestMintableERC20Token.sol | 10 + .../contracts/test/TestTransformerHost.sol | 60 ++ contracts/zero-ex/contracts/test/TestWeth.sol | 42 + .../test/TestWethTransformerHost.sol | 53 ++ contracts/zero-ex/package.json | 3 +- contracts/zero-ex/src/artifacts.ts | 14 +- contracts/zero-ex/src/constants.ts | 4 + contracts/zero-ex/src/index.ts | 9 +- .../zero-ex/src/transformer_data_encoders.ts | 114 +++ contracts/zero-ex/src/wrappers.ts | 7 +- contracts/zero-ex/test/artifacts.ts | 34 +- .../test/features/transform_erc20_test.ts | 3 +- .../fill_quote_transformer_test.ts | 849 ++++++++++++++++++ .../transformers/pay_taker_transformer.ts | 147 +++ .../transformers/weth_transformer_test.ts | 147 +++ contracts/zero-ex/test/wrappers.ts | 6 + contracts/zero-ex/tsconfig.json | 7 + 24 files changed, 2392 insertions(+), 37 deletions(-) create mode 100644 contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol create mode 100644 contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol create mode 100644 contracts/zero-ex/contracts/src/transformers/WethTransformer.sol create mode 100644 contracts/zero-ex/contracts/src/vendor/v3/IExchange.sol create mode 100644 contracts/zero-ex/contracts/test/TestFillQuoteTransformerExchange.sol create mode 100644 contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol create mode 100644 contracts/zero-ex/contracts/test/TestTransformerHost.sol create mode 100644 contracts/zero-ex/contracts/test/TestWeth.sol create mode 100644 contracts/zero-ex/contracts/test/TestWethTransformerHost.sol create mode 100644 contracts/zero-ex/src/constants.ts create mode 100644 contracts/zero-ex/src/transformer_data_encoders.ts create mode 100644 contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts create mode 100644 contracts/zero-ex/test/transformers/pay_taker_transformer.ts create mode 100644 contracts/zero-ex/test/transformers/weth_transformer_test.ts diff --git a/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol index f35179edd5..4ce8f16687 100644 --- a/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol +++ b/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol @@ -98,6 +98,21 @@ library LibTransformERC20RichErrors { ); } + // Common Transformer errors /////////////////////////////////////////////// + + function InvalidTransformDataError( + bytes memory transformData + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("InvalidTransformDataError(bytes)")), + transformData + ); + } + // FillQuoteTransformer errors ///////////////////////////////////////////// function IncompleteFillSellQuoteError( @@ -177,24 +192,7 @@ library LibTransformERC20RichErrors { ); } - // WethTransformer errors //////////////////////////////////////////////////// - - function WrongNumberOfTokensReceivedError( - uint256 actual, - uint256 expected - ) - internal - pure - returns (bytes memory) - { - return abi.encodeWithSelector( - bytes4(keccak256("WrongNumberOfTokensReceivedError(uint256,uint256)")), - actual, - expected - ); - } - - function InvalidTokenReceivedError( + function InvalidTakerFeeTokenError( address token ) internal @@ -202,8 +200,9 @@ library LibTransformERC20RichErrors { returns (bytes memory) { return abi.encodeWithSelector( - bytes4(keccak256("InvalidTokenReceivedError(address)")), + bytes4(keccak256("InvalidTakerFeeTokenError(address)")), token ); } + } diff --git a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol new file mode 100644 index 0000000000..f004222ea8 --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol @@ -0,0 +1,418 @@ +/* + + 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/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol"; +import "../errors/LibTransformERC20RichErrors.sol"; +import "../vendor/v3/IExchange.sol"; +import "./IERC20Transformer.sol"; +import "./LibERC20Transformer.sol"; + + +/// @dev A transformer that fills an ERC20 market sell/buy quote. +contract FillQuoteTransformer is + IERC20Transformer +{ + // solhint-disable indent,no-empty-blocks,no-unused-vars + + /// @dev Transform data to ABI-encode and pass into `transform()`. + struct TransformData { + // The token being sold. + // This should be an actual token, not the ETH pseudo-token. + IERC20TokenV06 sellToken; + // The token being bought. + // This should be an actual token, not the ETH pseudo-token. + IERC20TokenV06 buyToken; + // The orders to fill. + IExchange.Order[] orders; + // Signatures for each respective order in `orders`. + bytes[] signatures; + // Maximum fill amount for each order. This may be shorter than the + // number of orders, where missing entries will be treated as `uint256(-1)`. + // For sells, this will be the maximum sell amount (taker asset). + // For buys, this will be the maximum buy amount (maker asset). + uint256[] maxOrderFillAmounts; + // Amount of `sellToken` to sell. May be `uint256(-1)` to sell entire + // amount of `sellToken` received. Zero if performing a market buy. + uint256 sellAmount; + // Amount of `buyToken` to buy. Zero if performing a market sell. + uint256 buyAmount; + } + + /// @dev Results of a call to `_fillOrder()`. + struct FillOrderResults { + // The amount of taker tokens sold, according to balance checks. + uint256 takerTokenSoldAmount; + // The amount of maker tokens sold, according to balance checks. + uint256 makerTokenBoughtAmount; + // The amount of protocol fee paid. + uint256 protocolFeePaid; + } + + /// @dev The Exchange ERC20Proxy ID. + bytes4 constant private ERC20_ASSET_PROXY_ID = 0xf47261b0; + + /// @dev The Exchange contract. + IExchange public immutable exchange; + /// @dev The ERC20Proxy address. + address public immutable erc20Proxy; + + using LibERC20TokenV06 for IERC20TokenV06; + using LibERC20Transformer for IERC20TokenV06; + using LibSafeMathV06 for uint256; + using LibRichErrorsV06 for bytes; + + constructor(IExchange exchange_) public { + exchange = exchange_; + erc20Proxy = exchange_.getAssetProxy(ERC20_ASSET_PROXY_ID); + } + + /// @dev Sell this contract's entire balance of of `sellToken` in exchange + /// for `buyToken` by filling `orders`. Protocol fees should be attached + /// to this call. `buyToken` and excess ETH will be transferred back to the caller. + /// This function cannot be re-entered. + /// @param data_ ABI-encoded `TransformData`. + /// @return success `TRANSFORMER_SUCCESS` on success. + function transform( + bytes32, // callDataHash, + address payable, // taker, + bytes calldata data_ + ) + external + override + returns (bytes4 success) + { + TransformData memory data = abi.decode(data_, (TransformData)); + + // Validate data fields. + if (data.sellToken.isTokenETH() || + data.buyToken.isTokenETH() || + data.orders.length != data.signatures.length) + { + LibTransformERC20RichErrors.InvalidTransformDataError(data_).rrevert(); + } + + // If `sellAmount == -1` and `buyAmount == 0` then we are selling + // the entire balance of `sellToken`. This is useful in cases where + // the exact sell amount is not exactly known in advance, like when + // unwrapping Chai/cUSDC/cDAI. + if (data.sellAmount == uint256(-1) && data.buyAmount == 0) { + data.sellAmount = data.sellToken.getTokenBalanceOf(address(this)); + } + + // Approve the ERC20 proxy to spend `sellToken`. + data.sellToken.approveIfBelow(erc20Proxy, data.sellAmount); + + // Fill the orders. + uint256 singleProtocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice); + uint256 ethRemaining = address(this).balance; + uint256 boughtAmount = 0; + uint256 soldAmount = 0; + for (uint256 i = 0; i < data.orders.length; ++i) { + // Check if we've hit our targets. + if (data.buyAmount == 0) { + // Market sell check. + if (soldAmount >= data.sellAmount) { + break; + } + } else { + // Market buy check. + if (boughtAmount >= data.buyAmount) { + break; + } + } + + // Ensure we have enough ETH to cover the protocol fee. + if (ethRemaining < singleProtocolFee) { + LibTransformERC20RichErrors + .InsufficientProtocolFeeError(ethRemaining, singleProtocolFee) + .rrevert(); + } + + // Fill the order. + FillOrderResults memory results; + if (data.buyAmount == 0) { + // Market sell. + results = _sellToOrder( + data.buyToken, + data.sellToken, + data.orders[i], + data.signatures[i], + data.sellAmount.safeSub(soldAmount).min256( + data.maxOrderFillAmounts.length > i + ? data.maxOrderFillAmounts[i] + : uint256(-1) + ), + singleProtocolFee + ); + } else { + // Market buy. + results = _buyFromOrder( + data.buyToken, + data.sellToken, + data.orders[i], + data.signatures[i], + data.buyAmount.safeSub(boughtAmount).min256( + data.maxOrderFillAmounts.length > i + ? data.maxOrderFillAmounts[i] + : uint256(-1) + ), + singleProtocolFee + ); + } + + // Accumulate totals. + soldAmount = soldAmount.safeAdd(results.takerTokenSoldAmount); + boughtAmount = boughtAmount.safeAdd(results.makerTokenBoughtAmount); + ethRemaining = ethRemaining.safeSub(results.protocolFeePaid); + } + + // Ensure we hit our targets. + if (data.buyAmount == 0) { + // Market sell check. + if (soldAmount < data.sellAmount) { + LibTransformERC20RichErrors + .IncompleteFillSellQuoteError( + address(data.sellToken), + soldAmount, + data.sellAmount + ).rrevert(); + } + } else { + // Market buy check. + if (boughtAmount < data.buyAmount) { + LibTransformERC20RichErrors + .IncompleteFillBuyQuoteError( + address(data.buyToken), + boughtAmount, + data.buyAmount + ).rrevert(); + } + } + return LibERC20Transformer.TRANSFORMER_SUCCESS; + } + + /// @dev Try to sell up to `sellAmount` from an order. + /// @param makerToken The maker/buy token. + /// @param takerToken The taker/sell token. + /// @param order The order to fill. + /// @param signature The signature for `order`. + /// @param sellAmount Amount of taker token to sell. + /// @param protocolFee The protocol fee needed to fill `order`. + function _sellToOrder( + IERC20TokenV06 makerToken, + IERC20TokenV06 takerToken, + IExchange.Order memory order, + bytes memory signature, + uint256 sellAmount, + uint256 protocolFee + ) + private + returns (FillOrderResults memory results) + { + IERC20TokenV06 takerFeeToken = order.takerFeeAssetData.length == 0 + ? IERC20TokenV06(address(0)) + : _getTokenFromERC20AssetData(order.takerFeeAssetData); + + uint256 takerTokenFillAmount = sellAmount; + + if (order.takerFee != 0) { + if (takerFeeToken == makerToken) { + // Taker fee is payable in the maker token, so we need to + // approve the proxy to spend the maker token. + // It isn't worth computing the actual taker fee + // since `approveIfBelow()` will set the allowance to infinite. We + // just need a reasonable upper bound to avoid unnecessarily re-approving. + takerFeeToken.approveIfBelow(erc20Proxy, order.takerFee); + } else if (takerFeeToken == takerToken){ + // Taker fee is payable in the taker token, so we need to + // reduce the fill amount to cover the fee. + // takerTokenFillAmount' = + // (takerTokenFillAmount * order.takerAssetAmount) / + // (order.takerAssetAmount + order.takerFee) + takerTokenFillAmount = LibMathV06.getPartialAmountCeil( + order.takerAssetAmount, + order.takerAssetAmount.safeAdd(order.takerFee), + takerTokenFillAmount + ); + } else { + // Only support taker or maker asset denominated taker fees. + LibTransformERC20RichErrors.InvalidTakerFeeTokenError( + address(takerFeeToken) + ).rrevert(); + } + } + + // Clamp fill amount to order size. + takerTokenFillAmount = LibSafeMathV06.min256( + takerTokenFillAmount, + order.takerAssetAmount + ); + + // Perform the fill. + return _fillOrder( + order, + signature, + takerTokenFillAmount, + protocolFee, + makerToken, + takerFeeToken == takerToken + ); + } + + /// @dev Try to buy up to `buyAmount` from an order. + /// @param makerToken The maker/buy token. + /// @param takerToken The taker/sell token. + /// @param order The order to fill. + /// @param signature The signature for `order`. + /// @param buyAmount Amount of maker token to buy. + /// @param protocolFee The protocol fee needed to fill `order`. + function _buyFromOrder( + IERC20TokenV06 makerToken, + IERC20TokenV06 takerToken, + IExchange.Order memory order, + bytes memory signature, + uint256 buyAmount, + uint256 protocolFee + ) + private + returns (FillOrderResults memory results) + { + IERC20TokenV06 takerFeeToken = order.takerFeeAssetData.length == 0 + ? IERC20TokenV06(address(0)) + : _getTokenFromERC20AssetData(order.takerFeeAssetData); + + uint256 makerTokenFillAmount = buyAmount; + + if (order.takerFee != 0) { + if (takerFeeToken == makerToken) { + // Taker fee is payable in the maker token. + // Increase the fill amount to account for maker tokens being + // lost to the taker fee. + // makerTokenFillAmount' = + // (order.makerAssetAmount * makerTokenFillAmount) / + // (order.makerAssetAmount - order.takerFee) + makerTokenFillAmount = LibMathV06.getPartialAmountCeil( + order.makerAssetAmount, + order.makerAssetAmount.safeSub(order.takerFee), + makerTokenFillAmount + ); + // Approve the proxy to spend the maker token. + // It isn't worth computing the actual taker fee + // since `approveIfBelow()` will set the allowance to infinite. We + // just need a reasonable upper bound to avoid unnecessarily re-approving. + takerFeeToken.approveIfBelow(erc20Proxy, order.takerFee); + } else if (takerFeeToken != takerToken) { + // Only support taker or maker asset denominated taker fees. + LibTransformERC20RichErrors.InvalidTakerFeeTokenError( + address(takerFeeToken) + ).rrevert(); + } + } + + // Convert maker fill amount to taker fill amount. + uint256 takerTokenFillAmount = LibSafeMathV06.min256( + order.takerAssetAmount, + LibMathV06.getPartialAmountCeil( + makerTokenFillAmount, + order.makerAssetAmount, + order.takerAssetAmount + ) + ); + + // Perform the fill. + return _fillOrder( + order, + signature, + takerTokenFillAmount, + protocolFee, + makerToken, + takerFeeToken == takerToken + ); + } + + /// @dev Attempt to fill an order. If the fill reverts, the revert will be + /// swallowed and `results` will be zeroed out. + /// @param order The order to fill. + /// @param signature The order signature. + /// @param takerAssetFillAmount How much taker asset to fill. + /// @param protocolFee The protocol fee needed to fill this order. + /// @param makerToken The maker token. + /// @param isTakerFeeInTakerToken Whether the taker fee token is the same as the + /// taker token. + function _fillOrder( + IExchange.Order memory order, + bytes memory signature, + uint256 takerAssetFillAmount, + uint256 protocolFee, + IERC20TokenV06 makerToken, + bool isTakerFeeInTakerToken + ) + private + returns (FillOrderResults memory results) + { + // Track changes in the maker token balance. + results.makerTokenBoughtAmount = makerToken.balanceOf(address(this)); + try + exchange.fillOrder + {value: protocolFee} + (order, takerAssetFillAmount, signature) + returns (IExchange.FillResults memory fillResults) + { + // Update maker quantity based on changes in token balances. + results.makerTokenBoughtAmount = makerToken.balanceOf(address(this)) + .safeSub(results.makerTokenBoughtAmount); + // We can trust the other fill result quantities. + results.protocolFeePaid = fillResults.protocolFeePaid; + results.takerTokenSoldAmount = fillResults.takerAssetFilledAmount; + // If the taker fee is payable in the taker asset, include the + // taker fee in the total amount sold. + if (isTakerFeeInTakerToken) { + results.takerTokenSoldAmount = + results.takerTokenSoldAmount.safeAdd(fillResults.takerFeePaid); + } + } catch (bytes memory) { + // If the fill fails, zero out fill quantities. + results.makerTokenBoughtAmount = 0; + } + } + + /// @dev Extract the token from plain ERC20 asset data. + /// @param assetData The order asset data. + function _getTokenFromERC20AssetData(bytes memory assetData) + private + pure + returns (IERC20TokenV06 token) + { + if (assetData.length != 36 || + LibBytesV06.readBytes4(assetData, 0) != ERC20_ASSET_PROXY_ID) + { + LibTransformERC20RichErrors + .InvalidERC20AssetDataError(assetData) + .rrevert(); + } + return IERC20TokenV06(LibBytesV06.readAddress(assetData, 16)); + } +} diff --git a/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol new file mode 100644 index 0000000000..4b668862c1 --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol @@ -0,0 +1,77 @@ +/* + + 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/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; +import "../errors/LibTransformERC20RichErrors.sol"; +import "./IERC20Transformer.sol"; +import "./LibERC20Transformer.sol"; + + +/// @dev A transformer that transfers tokens to the taker. +contract PayTakerTransformer is + IERC20Transformer +{ + /// @dev Transform data to ABI-encode and pass into `transform()`. + struct TransformData { + // The tokens to transfer to the taker. + IERC20TokenV06[] tokens; + // Amount of each token in `tokens` to transfer to the taker. + // `uint(-1)` will transfer the entire balance. + uint256[] amounts; + } + + using LibRichErrorsV06 for bytes; + using LibSafeMathV06 for uint256; + using LibERC20Transformer for IERC20TokenV06; + + /// @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 success `TRANSFORMER_SUCCESS` on success. + function transform( + bytes32, // callDataHash, + address payable taker, + bytes calldata data_ + ) + external + override + returns (bytes4 success) + { + TransformData memory data = abi.decode(data_, (TransformData)); + + // Transfer tokens directly to the taker. + for (uint256 i = 0; i < data.tokens.length; ++i) { + // The `amounts` array can be shorter than the `tokens` array. + // Missing elements are treated as `uint256(-1)`. + uint256 amount = data.amounts.length > i ? data.amounts[i] : uint256(-1); + if (amount == uint256(-1)) { + amount = data.tokens[i].getTokenBalanceOf(address(this)); + } + if (amount != 0) { + data.tokens[i].transformerTransfer(taker, amount); + } + } + return LibERC20Transformer.TRANSFORMER_SUCCESS; + } +} diff --git a/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol b/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol new file mode 100644 index 0000000000..003da666e5 --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol @@ -0,0 +1,91 @@ +/* + + 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/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; +import "../errors/LibTransformERC20RichErrors.sol"; +import "./IERC20Transformer.sol"; +import "./LibERC20Transformer.sol"; + + +/// @dev A transformer that wraps or unwraps WETH. +contract WethTransformer is + IERC20Transformer +{ + /// @dev Transform data to ABI-encode and pass into `transform()`. + struct TransformData { + // The token to wrap/unwrap. Must be either ETH or WETH. + IERC20TokenV06 token; + // Amount of `token` to wrap or unwrap. + // `uint(-1)` will unwrap the entire balance. + uint256 amount; + } + + // solhint-disable + /// @dev The WETH contract address. + IEtherTokenV06 public immutable weth; + // solhint-enable + + using LibRichErrorsV06 for bytes; + using LibSafeMathV06 for uint256; + using LibERC20Transformer for IERC20TokenV06; + + /// @dev Construct the transformer and store the WETH address in an immutable. + /// @param weth_ The weth token. + constructor(IEtherTokenV06 weth_) public { + weth = weth_; + } + + /// @dev Wraps and unwraps WETH. + /// @param data_ ABI-encoded `TransformData`, indicating which token to wrap/umwrap. + /// @return success `TRANSFORMER_SUCCESS` on success. + function transform( + bytes32, // callDataHash, + address payable, // taker, + bytes calldata data_ + ) + external + override + returns (bytes4 success) + { + TransformData memory data = abi.decode(data_, (TransformData)); + if (!data.token.isTokenETH() && data.token != weth) { + LibTransformERC20RichErrors.InvalidTransformDataError(data_).rrevert(); + } + + uint256 amount = data.amount; + if (amount == uint256(-1)) { + amount = data.token.getTokenBalanceOf(address(this)); + } + + if (amount != 0) { + if (data.token.isTokenETH()) { + // Wrap ETH. + weth.deposit{value: amount}(); + } else { + // Unwrap WETH. + weth.withdraw(amount); + } + } + return LibERC20Transformer.TRANSFORMER_SUCCESS; + } +} diff --git a/contracts/zero-ex/contracts/src/vendor/v3/IExchange.sol b/contracts/zero-ex/contracts/src/vendor/v3/IExchange.sol new file mode 100644 index 0000000000..7d98b61b87 --- /dev/null +++ b/contracts/zero-ex/contracts/src/vendor/v3/IExchange.sol @@ -0,0 +1,107 @@ +/* + + 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 Interface to the V3 Exchange. +interface IExchange { + + /// @dev V3 Order structure. + struct Order { + // Address that created the order. + address makerAddress; + // Address that is allowed to fill the order. + // If set to 0, any address is allowed to fill the order. + address takerAddress; + // Address that will recieve fees when order is filled. + address feeRecipientAddress; + // Address that is allowed to call Exchange contract methods that affect this order. + // If set to 0, any address is allowed to call these methods. + address senderAddress; + // Amount of makerAsset being offered by maker. Must be greater than 0. + uint256 makerAssetAmount; + // Amount of takerAsset being bid on by maker. Must be greater than 0. + uint256 takerAssetAmount; + // Fee paid to feeRecipient by maker when order is filled. + uint256 makerFee; + // Fee paid to feeRecipient by taker when order is filled. + uint256 takerFee; + // Timestamp in seconds at which order expires. + uint256 expirationTimeSeconds; + // Arbitrary number to facilitate uniqueness of the order's hash. + uint256 salt; + // Encoded data that can be decoded by a specified proxy contract when transferring makerAsset. + // The leading bytes4 references the id of the asset proxy. + bytes makerAssetData; + // Encoded data that can be decoded by a specified proxy contract when transferring takerAsset. + // The leading bytes4 references the id of the asset proxy. + bytes takerAssetData; + // Encoded data that can be decoded by a specified proxy contract when transferring makerFeeAsset. + // The leading bytes4 references the id of the asset proxy. + bytes makerFeeAssetData; + // Encoded data that can be decoded by a specified proxy contract when transferring takerFeeAsset. + // The leading bytes4 references the id of the asset proxy. + bytes takerFeeAssetData; + } + + /// @dev V3 `fillOrder()` results.` + struct FillResults { + // Total amount of makerAsset(s) filled. + uint256 makerAssetFilledAmount; + // Total amount of takerAsset(s) filled. + uint256 takerAssetFilledAmount; + // Total amount of fees paid by maker(s) to feeRecipient(s). + uint256 makerFeePaid; + // Total amount of fees paid by taker to feeRecipients(s). + uint256 takerFeePaid; + // Total amount of fees paid by taker to the staking contract. + uint256 protocolFeePaid; + } + + /// @dev Fills the input order. + /// @param order Order struct containing order specifications. + /// @param takerAssetFillAmount Desired amount of takerAsset to sell. + /// @param signature Proof that order has been created by maker. + /// @return fillResults Amounts filled and fees paid by maker and taker. + function fillOrder( + Order calldata order, + uint256 takerAssetFillAmount, + bytes calldata signature + ) + external + payable + returns (FillResults memory fillResults); + + /// @dev Returns the protocolFeeMultiplier + /// @return multiplier The multiplier for protocol fees. + function protocolFeeMultiplier() + external + view + returns (uint256 multiplier); + + /// @dev Gets an asset proxy. + /// @param assetProxyId Id of the asset proxy. + /// @return proxyAddress The asset proxy registered to assetProxyId. + /// Returns 0x0 if no proxy is registered. + function getAssetProxy(bytes4 assetProxyId) + external + view + returns (address proxyAddress); +} diff --git a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerExchange.sol b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerExchange.sol new file mode 100644 index 0000000000..698ff8c364 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerExchange.sol @@ -0,0 +1,145 @@ +/* + + 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/LibBytesV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "../src/vendor/v3/IExchange.sol"; +import "./TestMintableERC20Token.sol"; + + +contract TestFillQuoteTransformerExchange { + + struct FillBehavior { + // How much of the order is filled, in taker asset amount. + uint256 filledTakerAssetAmount; + // Scaling for maker assets minted, in 1e18. + uint256 makerAssetMintRatio; + } + + uint256 private constant PROTOCOL_FEE_MULTIPLIER = 1337; + + using LibSafeMathV06 for uint256; + + function fillOrder( + IExchange.Order calldata order, + uint256 takerAssetFillAmount, + bytes calldata signature + ) + external + payable + returns (IExchange.FillResults memory fillResults) + { + require( + signature.length != 0, + "TestFillQuoteTransformerExchange/INVALID_SIGNATURE" + ); + // The signature is the ABI-encoded FillBehavior data. + FillBehavior memory behavior = abi.decode(signature, (FillBehavior)); + + uint256 protocolFee = PROTOCOL_FEE_MULTIPLIER * tx.gasprice; + require( + msg.value == protocolFee, + "TestFillQuoteTransformerExchange/INSUFFICIENT_PROTOCOL_FEE" + ); + // Return excess protocol fee. + msg.sender.transfer(msg.value - protocolFee); + + // Take taker tokens. + TestMintableERC20Token takerToken = _getTokenFromAssetData(order.takerAssetData); + takerAssetFillAmount = LibSafeMathV06.min256( + order.takerAssetAmount.safeSub(behavior.filledTakerAssetAmount), + takerAssetFillAmount + ); + require( + takerToken.getSpendableAmount(msg.sender, address(this)) >= takerAssetFillAmount, + "TestFillQuoteTransformerExchange/INSUFFICIENT_TAKER_FUNDS" + ); + takerToken.transferFrom(msg.sender, order.makerAddress, takerAssetFillAmount); + + // Mint maker tokens. + uint256 makerAssetFilledAmount = LibMathV06.getPartialAmountFloor( + takerAssetFillAmount, + order.takerAssetAmount, + order.makerAssetAmount + ); + TestMintableERC20Token makerToken = _getTokenFromAssetData(order.makerAssetData); + makerToken.mint( + msg.sender, + LibMathV06.getPartialAmountFloor( + behavior.makerAssetMintRatio, + 1e18, + makerAssetFilledAmount + ) + ); + + // Take taker fee. + TestMintableERC20Token takerFeeToken = _getTokenFromAssetData(order.takerFeeAssetData); + uint256 takerFee = LibMathV06.getPartialAmountFloor( + takerAssetFillAmount, + order.takerAssetAmount, + order.takerFee + ); + require( + takerFeeToken.getSpendableAmount(msg.sender, address(this)) >= takerFee, + "TestFillQuoteTransformerExchange/INSUFFICIENT_TAKER_FEE_FUNDS" + ); + takerFeeToken.transferFrom(msg.sender, order.feeRecipientAddress, takerFee); + + fillResults.makerAssetFilledAmount = makerAssetFilledAmount; + fillResults.takerAssetFilledAmount = takerAssetFillAmount; + fillResults.makerFeePaid = uint256(-1); + fillResults.takerFeePaid = takerFee; + fillResults.protocolFeePaid = protocolFee; + } + + function encodeBehaviorData(FillBehavior calldata behavior) + external + pure + returns (bytes memory encoded) + { + return abi.encode(behavior); + } + + function protocolFeeMultiplier() + external + pure + returns (uint256) + { + return PROTOCOL_FEE_MULTIPLIER; + } + + function getAssetProxy(bytes4) + external + view + returns (address) + { + return address(this); + } + + function _getTokenFromAssetData(bytes memory assetData) + private + pure + returns (TestMintableERC20Token token) + { + return TestMintableERC20Token(LibBytesV06.readAddress(assetData, 16)); + } +} diff --git a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol new file mode 100644 index 0000000000..6b85acd2f5 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol @@ -0,0 +1,45 @@ +/* + + 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/IERC20Transformer.sol"; +import "./TestMintableERC20Token.sol"; +import "./TestTransformerHost.sol"; + + +contract TestFillQuoteTransformerHost is + TestTransformerHost +{ + function executeTransform( + IERC20Transformer transformer, + TestMintableERC20Token inputToken, + uint256 inputTokenAmount, + bytes calldata data + ) + external + payable + { + if (inputTokenAmount != 0) { + inputToken.mint(address(this), inputTokenAmount); + } + // Have to make this call externally because transformers aren't payable. + this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data); + } +} diff --git a/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol b/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol index 27ef1209fd..f2015c6b70 100644 --- a/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol +++ b/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol @@ -74,4 +74,14 @@ contract TestMintableERC20Token { balanceOf[to] += amount; return true; } + + function getSpendableAmount(address owner, address spender) + external + view + returns (uint256) + { + return balanceOf[owner] < allowance[owner][spender] + ? balanceOf[owner] + : allowance[owner][spender]; + } } diff --git a/contracts/zero-ex/contracts/test/TestTransformerHost.sol b/contracts/zero-ex/contracts/test/TestTransformerHost.sol new file mode 100644 index 0000000000..23f2fff764 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestTransformerHost.sol @@ -0,0 +1,60 @@ +/* + + 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/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "../src/transformers/IERC20Transformer.sol"; +import "../src/transformers/LibERC20Transformer.sol"; + + +contract TestTransformerHost { + + using LibERC20Transformer for IERC20TokenV06; + using LibRichErrorsV06 for bytes; + + function rawExecuteTransform( + IERC20Transformer transformer, + bytes32 callDataHash, + address taker, + bytes calldata data + ) + external + { + (bool success, bytes memory resultData) = + address(transformer).delegatecall(abi.encodeWithSelector( + transformer.transform.selector, + callDataHash, + taker, + data + )); + if (!success) { + resultData.rrevert(); + } + require( + abi.decode(resultData, (bytes4)) == LibERC20Transformer.TRANSFORMER_SUCCESS, + "TestFillQuoteTransformerTaker/UNSUCCESSFUL_RESULT" + ); + } + + // solhint-disable + receive() external payable {} + // solhint-enable +} diff --git a/contracts/zero-ex/contracts/test/TestWeth.sol b/contracts/zero-ex/contracts/test/TestWeth.sol new file mode 100644 index 0000000000..5a4f622bb5 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestWeth.sol @@ -0,0 +1,42 @@ +/* + + 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 "./TestMintableERC20Token.sol"; + + +contract TestWeth is + TestMintableERC20Token +{ + function deposit() + external + payable + { + this.mint(msg.sender, msg.value); + } + + function withdraw(uint256 amount) + external + { + require(balanceOf[msg.sender] >= amount, "TestWeth/INSUFFICIENT_FUNDS"); + balanceOf[msg.sender] -= amount; + msg.sender.transfer(amount); + } +} diff --git a/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol b/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol new file mode 100644 index 0000000000..3c0fd83999 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol @@ -0,0 +1,53 @@ +/* + + 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/IERC20Transformer.sol"; +import "./TestMintableERC20Token.sol"; +import "./TestTransformerHost.sol"; +import "./TestWeth.sol"; + + +contract TestWethTransformerHost is + TestTransformerHost +{ + // solhint-disable + TestWeth private immutable _weth; + // solhint-enable + + constructor(TestWeth weth) public { + _weth = weth; + } + + function executeTransform( + uint256 wethAmount, + IERC20Transformer transformer, + bytes calldata data + ) + external + payable + { + if (wethAmount != 0) { + _weth.deposit{value: wethAmount}(); + } + // Have to make this call externally because transformers aren't payable. + this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data); + } +} diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index d1f05183e8..6a69f8eb38 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -38,7 +38,7 @@ "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" }, "config": { - "publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20", + "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|FixinCommon|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Transformer|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|SimpleFunctionRegistry|TestCallTarget|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestZeroExFeature|TokenSpender|TransformERC20|ZeroEx).json" }, @@ -56,6 +56,7 @@ "@0x/contracts-gen": "^2.0.8", "@0x/contracts-test-utils": "^5.3.2", "@0x/dev-utils": "^3.2.1", + "@0x/order-utils": "^10.2.4", "@0x/sol-compiler": "^4.0.8", "@0x/subproviders": "^6.0.8", "@0x/ts-doc-gen": "^0.0.22", diff --git a/contracts/zero-ex/src/artifacts.ts b/contracts/zero-ex/src/artifacts.ts index fea6a4beb1..f68d20e56b 100644 --- a/contracts/zero-ex/src/artifacts.ts +++ b/contracts/zero-ex/src/artifacts.ts @@ -5,25 +5,31 @@ */ import { ContractArtifact } from 'ethereum-types'; +import * as FillQuoteTransformer from '../generated-artifacts/FillQuoteTransformer.json'; import * as FullMigration from '../generated-artifacts/FullMigration.json'; -import * as IAllowanceTarget from '../generated-artifacts/IAllowanceTarget.json'; import * as IERC20Transformer from '../generated-artifacts/IERC20Transformer.json'; -import * as IFlashWallet from '../generated-artifacts/IFlashWallet.json'; import * as InitialMigration from '../generated-artifacts/InitialMigration.json'; import * as IOwnable from '../generated-artifacts/IOwnable.json'; import * as ISimpleFunctionRegistry from '../generated-artifacts/ISimpleFunctionRegistry.json'; import * as ITokenSpender from '../generated-artifacts/ITokenSpender.json'; import * as ITransformERC20 from '../generated-artifacts/ITransformERC20.json'; +import * as LibERC20Transformer from '../generated-artifacts/LibERC20Transformer.json'; +import * as PayTakerTransformer from '../generated-artifacts/PayTakerTransformer.json'; +import * as Puppet from '../generated-artifacts/Puppet.json'; +import * as WethTransformer from '../generated-artifacts/WethTransformer.json'; import * as ZeroEx from '../generated-artifacts/ZeroEx.json'; export const artifacts = { ZeroEx: ZeroEx as ContractArtifact, FullMigration: FullMigration as ContractArtifact, InitialMigration: InitialMigration as ContractArtifact, - IFlashWallet: IFlashWallet as ContractArtifact, - IAllowanceTarget: IAllowanceTarget as ContractArtifact, + Puppet: Puppet as ContractArtifact, IERC20Transformer: IERC20Transformer as ContractArtifact, IOwnable: IOwnable as ContractArtifact, ISimpleFunctionRegistry: ISimpleFunctionRegistry as ContractArtifact, ITokenSpender: ITokenSpender as ContractArtifact, ITransformERC20: ITransformERC20 as ContractArtifact, + LibERC20Transformer: LibERC20Transformer as ContractArtifact, + PayTakerTransformer: PayTakerTransformer as ContractArtifact, + WethTransformer: WethTransformer as ContractArtifact, + FillQuoteTransformer: FillQuoteTransformer as ContractArtifact, }; diff --git a/contracts/zero-ex/src/constants.ts b/contracts/zero-ex/src/constants.ts new file mode 100644 index 0000000000..2c49072817 --- /dev/null +++ b/contracts/zero-ex/src/constants.ts @@ -0,0 +1,4 @@ +/* + * The pseudo-token address for ETH used by `tranformERC20()`. + */ +export const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; diff --git a/contracts/zero-ex/src/index.ts b/contracts/zero-ex/src/index.ts index b9e4913d08..c2c9cebc48 100644 --- a/contracts/zero-ex/src/index.ts +++ b/contracts/zero-ex/src/index.ts @@ -1,9 +1,14 @@ export { artifacts } from './artifacts'; export { + FillQuoteTransformerContract, IOwnableContract, IOwnableEvents, ISimpleFunctionRegistryContract, ISimpleFunctionRegistryEvents, + ITokenSpenderContract, + ITransformERC20Contract, + PayTakerTransformerContract, + WethTransformerContract, ZeroExContract, } from './wrappers'; export { ZeroExRevertErrors } from '@0x/utils'; @@ -36,4 +41,6 @@ export { TupleDataItem, StateMutability, } from 'ethereum-types'; -export { rlpEncodeNonce } from './nonce_utils'; + +export * from './constants'; +export * from './transformer_data_encoders'; diff --git a/contracts/zero-ex/src/transformer_data_encoders.ts b/contracts/zero-ex/src/transformer_data_encoders.ts new file mode 100644 index 0000000000..b44bea7631 --- /dev/null +++ b/contracts/zero-ex/src/transformer_data_encoders.ts @@ -0,0 +1,114 @@ +import { Order } from '@0x/types'; +import { AbiEncoder, BigNumber } from '@0x/utils'; + +const ORDER_ABI_COMPONENTS = [ + { name: 'makerAddress', type: 'address' }, + { name: 'takerAddress', type: 'address' }, + { name: 'feeRecipientAddress', type: 'address' }, + { name: 'senderAddress', type: 'address' }, + { name: 'makerAssetAmount', type: 'uint256' }, + { name: 'takerAssetAmount', type: 'uint256' }, + { name: 'makerFee', type: 'uint256' }, + { name: 'takerFee', type: 'uint256' }, + { name: 'expirationTimeSeconds', type: 'uint256' }, + { name: 'salt', type: 'uint256' }, + { name: 'makerAssetData', type: 'bytes' }, + { name: 'takerAssetData', type: 'bytes' }, + { name: 'makerFeeAssetData', type: 'bytes' }, + { name: 'takerFeeAssetData', type: 'bytes' }, +]; + +/** + * ABI encoder for `FillQuoteTransformer.TransformData` + */ +export const fillQuoteTransformerDataEncoder = AbiEncoder.create([ + { + name: 'data', + type: 'tuple', + components: [ + { name: 'sellToken', type: 'address' }, + { name: 'buyToken', type: 'address' }, + { + name: 'orders', + type: 'tuple[]', + components: ORDER_ABI_COMPONENTS, + }, + { name: 'signatures', type: 'bytes[]' }, + { name: 'maxOrderFillAmounts', type: 'uint256[]' }, + { name: 'sellAmount', type: 'uint256' }, + { name: 'buyAmount', type: 'uint256' }, + ], + }, +]); + +/** + * `FillQuoteTransformer.TransformData` + */ +export interface FillQuoteTransformerData { + sellToken: string; + buyToken: string; + orders: Array>; + signatures: string[]; + maxOrderFillAmounts: BigNumber[]; + sellAmount: BigNumber; + buyAmount: BigNumber; +} + +/** + * ABI-encode a `FillQuoteTransformer.TransformData` type. + */ +export function encodeFillQuoteTransformerData(data: FillQuoteTransformerData): string { + return fillQuoteTransformerDataEncoder.encode([data]); +} + +/** + * ABI encoder for `WethTransformer.TransformData` + */ +export const wethTransformerDataEncoder = AbiEncoder.create([ + { + name: 'data', + type: 'tuple', + components: [{ name: 'token', type: 'address' }, { name: 'amount', type: 'uint256' }], + }, +]); + +/** + * `WethTransformer.TransformData` + */ +export interface WethTransformerData { + token: string; + amount: BigNumber; +} + +/** + * ABI-encode a `WethTransformer.TransformData` type. + */ +export function encodeWethTransformerData(data: WethTransformerData): string { + return wethTransformerDataEncoder.encode([data]); +} + +/** + * ABI encoder for `PayTakerTransformer.TransformData` + */ +export const payTakerTransformerDataEncoder = AbiEncoder.create([ + { + name: 'data', + type: 'tuple', + components: [{ name: 'tokens', type: 'address[]' }, { name: 'amounts', type: 'uint256[]' }], + }, +]); + +/** + * `PayTakerTransformer.TransformData` + */ +export interface PayTakerTransformerData { + tokens: string[]; + amounts: BigNumber[]; +} + +/** + * ABI-encode a `PayTakerTransformer.TransformData` type. + */ +export function encodePayTakerTransformerData(data: PayTakerTransformerData): string { + return payTakerTransformerDataEncoder.encode([data]); +} diff --git a/contracts/zero-ex/src/wrappers.ts b/contracts/zero-ex/src/wrappers.ts index 7be4d42f1e..fdbf2d49c0 100644 --- a/contracts/zero-ex/src/wrappers.ts +++ b/contracts/zero-ex/src/wrappers.ts @@ -3,13 +3,16 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ +export * from '../generated-wrappers/fill_quote_transformer'; export * from '../generated-wrappers/full_migration'; -export * from '../generated-wrappers/i_allowance_target'; export * from '../generated-wrappers/i_erc20_transformer'; -export * from '../generated-wrappers/i_flash_wallet'; export * from '../generated-wrappers/i_ownable'; export * from '../generated-wrappers/i_simple_function_registry'; export * from '../generated-wrappers/i_token_spender'; export * from '../generated-wrappers/i_transform_erc20'; export * from '../generated-wrappers/initial_migration'; +export * from '../generated-wrappers/lib_erc20_transformer'; +export * from '../generated-wrappers/pay_taker_transformer'; +export * from '../generated-wrappers/puppet'; +export * from '../generated-wrappers/weth_transformer'; export * from '../generated-wrappers/zero_ex'; diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index cf9c020daa..26a642efb7 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -7,16 +7,17 @@ import { ContractArtifact } from 'ethereum-types'; import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.json'; import * as Bootstrap from '../test/generated-artifacts/Bootstrap.json'; +import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json'; import * as FixinCommon from '../test/generated-artifacts/FixinCommon.json'; -import * as FlashWallet from '../test/generated-artifacts/FlashWallet.json'; import * as FullMigration from '../test/generated-artifacts/FullMigration.json'; import * as IAllowanceTarget from '../test/generated-artifacts/IAllowanceTarget.json'; import * as IBootstrap from '../test/generated-artifacts/IBootstrap.json'; import * as IERC20Transformer from '../test/generated-artifacts/IERC20Transformer.json'; +import * as IExchange from '../test/generated-artifacts/IExchange.json'; import * as IFeature from '../test/generated-artifacts/IFeature.json'; -import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json'; import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json'; import * as IOwnable from '../test/generated-artifacts/IOwnable.json'; +import * as IPuppet from '../test/generated-artifacts/IPuppet.json'; import * as ISimpleFunctionRegistry from '../test/generated-artifacts/ISimpleFunctionRegistry.json'; import * as ITestSimpleFunctionRegistryFeature from '../test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json'; import * as ITokenSpender from '../test/generated-artifacts/ITokenSpender.json'; @@ -29,6 +30,7 @@ import * as LibOwnableRichErrors from '../test/generated-artifacts/LibOwnableRic import * as LibOwnableStorage from '../test/generated-artifacts/LibOwnableStorage.json'; import * as LibProxyRichErrors from '../test/generated-artifacts/LibProxyRichErrors.json'; import * as LibProxyStorage from '../test/generated-artifacts/LibProxyStorage.json'; +import * as LibPuppetRichErrors from '../test/generated-artifacts/LibPuppetRichErrors.json'; import * as LibSimpleFunctionRegistryRichErrors from '../test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json'; import * as LibSimpleFunctionRegistryStorage from '../test/generated-artifacts/LibSimpleFunctionRegistryStorage.json'; import * as LibSpenderRichErrors from '../test/generated-artifacts/LibSpenderRichErrors.json'; @@ -36,37 +38,44 @@ import * as LibStorage from '../test/generated-artifacts/LibStorage.json'; import * as LibTokenSpenderStorage from '../test/generated-artifacts/LibTokenSpenderStorage.json'; import * as LibTransformERC20RichErrors from '../test/generated-artifacts/LibTransformERC20RichErrors.json'; import * as LibTransformERC20Storage from '../test/generated-artifacts/LibTransformERC20Storage.json'; -import * as LibWalletRichErrors from '../test/generated-artifacts/LibWalletRichErrors.json'; import * as Ownable from '../test/generated-artifacts/Ownable.json'; +import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json'; +import * as Puppet from '../test/generated-artifacts/Puppet.json'; import * as SimpleFunctionRegistry from '../test/generated-artifacts/SimpleFunctionRegistry.json'; -import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; +import * as TestFillQuoteTransformerExchange from '../test/generated-artifacts/TestFillQuoteTransformerExchange.json'; +import * as TestFillQuoteTransformerHost from '../test/generated-artifacts/TestFillQuoteTransformerHost.json'; import * as TestFullMigration from '../test/generated-artifacts/TestFullMigration.json'; import * as TestInitialMigration from '../test/generated-artifacts/TestInitialMigration.json'; import * as TestMigrator from '../test/generated-artifacts/TestMigrator.json'; import * as TestMintableERC20Token from '../test/generated-artifacts/TestMintableERC20Token.json'; import * as TestMintTokenERC20Transformer from '../test/generated-artifacts/TestMintTokenERC20Transformer.json'; +import * as TestPuppetTarget from '../test/generated-artifacts/TestPuppetTarget.json'; import * as TestSimpleFunctionRegistryFeatureImpl1 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl1.json'; import * as TestSimpleFunctionRegistryFeatureImpl2 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl2.json'; import * as TestTokenSpender from '../test/generated-artifacts/TestTokenSpender.json'; import * as TestTokenSpenderERC20Token from '../test/generated-artifacts/TestTokenSpenderERC20Token.json'; import * as TestTransformERC20 from '../test/generated-artifacts/TestTransformERC20.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'; import * as TestZeroExFeature from '../test/generated-artifacts/TestZeroExFeature.json'; import * as TokenSpender from '../test/generated-artifacts/TokenSpender.json'; import * as TransformERC20 from '../test/generated-artifacts/TransformERC20.json'; +import * as WethTransformer from '../test/generated-artifacts/WethTransformer.json'; import * as ZeroEx from '../test/generated-artifacts/ZeroEx.json'; export const artifacts = { ZeroEx: ZeroEx as ContractArtifact, LibCommonRichErrors: LibCommonRichErrors as ContractArtifact, LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact, LibProxyRichErrors: LibProxyRichErrors as ContractArtifact, + LibPuppetRichErrors: LibPuppetRichErrors as ContractArtifact, LibSimpleFunctionRegistryRichErrors: LibSimpleFunctionRegistryRichErrors as ContractArtifact, LibSpenderRichErrors: LibSpenderRichErrors as ContractArtifact, LibTransformERC20RichErrors: LibTransformERC20RichErrors as ContractArtifact, - LibWalletRichErrors: LibWalletRichErrors as ContractArtifact, AllowanceTarget: AllowanceTarget as ContractArtifact, - FlashWallet: FlashWallet as ContractArtifact, IAllowanceTarget: IAllowanceTarget as ContractArtifact, - IFlashWallet: IFlashWallet as ContractArtifact, + IPuppet: IPuppet as ContractArtifact, + Puppet: Puppet as ContractArtifact, Bootstrap: Bootstrap as ContractArtifact, IBootstrap: IBootstrap as ContractArtifact, IFeature: IFeature as ContractArtifact, @@ -89,19 +98,28 @@ export const artifacts = { LibStorage: LibStorage as ContractArtifact, LibTokenSpenderStorage: LibTokenSpenderStorage as ContractArtifact, LibTransformERC20Storage: LibTransformERC20Storage as ContractArtifact, + FillQuoteTransformer: FillQuoteTransformer as ContractArtifact, IERC20Transformer: IERC20Transformer as ContractArtifact, LibERC20Transformer: LibERC20Transformer as ContractArtifact, + PayTakerTransformer: PayTakerTransformer as ContractArtifact, + WethTransformer: WethTransformer as ContractArtifact, + IExchange: IExchange as ContractArtifact, ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact, - TestCallTarget: TestCallTarget as ContractArtifact, + TestFillQuoteTransformerExchange: TestFillQuoteTransformerExchange as ContractArtifact, + TestFillQuoteTransformerHost: TestFillQuoteTransformerHost as ContractArtifact, TestFullMigration: TestFullMigration as ContractArtifact, TestInitialMigration: TestInitialMigration as ContractArtifact, TestMigrator: TestMigrator as ContractArtifact, TestMintTokenERC20Transformer: TestMintTokenERC20Transformer as ContractArtifact, TestMintableERC20Token: TestMintableERC20Token as ContractArtifact, + TestPuppetTarget: TestPuppetTarget as ContractArtifact, TestSimpleFunctionRegistryFeatureImpl1: TestSimpleFunctionRegistryFeatureImpl1 as ContractArtifact, TestSimpleFunctionRegistryFeatureImpl2: TestSimpleFunctionRegistryFeatureImpl2 as ContractArtifact, TestTokenSpender: TestTokenSpender as ContractArtifact, TestTokenSpenderERC20Token: TestTokenSpenderERC20Token as ContractArtifact, TestTransformERC20: TestTransformERC20 as ContractArtifact, + TestTransformerHost: TestTransformerHost as ContractArtifact, + TestWeth: TestWeth as ContractArtifact, + TestWethTransformerHost: TestWethTransformerHost as ContractArtifact, TestZeroExFeature: TestZeroExFeature as ContractArtifact, }; diff --git a/contracts/zero-ex/test/features/transform_erc20_test.ts b/contracts/zero-ex/test/features/transform_erc20_test.ts index 28daa667f0..82bb3aee90 100644 --- a/contracts/zero-ex/test/features/transform_erc20_test.ts +++ b/contracts/zero-ex/test/features/transform_erc20_test.ts @@ -10,6 +10,7 @@ import { } from '@0x/contracts-test-utils'; import { AbiEncoder, hexUtils, 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'; @@ -206,8 +207,6 @@ blockchainTests.resets('TransformERC20 feature', env => { ); }); - const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; - it("succeeds if taker's output token balance increases by exactly minOutputTokenAmount, with ETH", async () => { const startingInputTokenBalance = getRandomInteger(0, '100e18'); await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync(); diff --git a/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts new file mode 100644 index 0000000000..05e920a6b0 --- /dev/null +++ b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts @@ -0,0 +1,849 @@ +import { + assertIntegerRoughlyEquals, + blockchainTests, + constants, + expect, + getRandomInteger, + Numberish, + randomAddress, +} from '@0x/contracts-test-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { Order } from '@0x/types'; +import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils'; +import * as _ from 'lodash'; + +import { encodeFillQuoteTransformerData, FillQuoteTransformerData } from '../../src/transformer_data_encoders'; +import { artifacts } from '../artifacts'; +import { + FillQuoteTransformerContract, + TestFillQuoteTransformerExchangeContract, + TestFillQuoteTransformerHostContract, + TestMintableERC20TokenContract, +} from '../wrappers'; + +const { NULL_ADDRESS, NULL_BYTES, MAX_UINT256, ZERO_AMOUNT } = constants; + +blockchainTests.resets('FillQuoteTransformer', env => { + let maker: string; + let feeRecipient: string; + let exchange: TestFillQuoteTransformerExchangeContract; + let transformer: FillQuoteTransformerContract; + let host: TestFillQuoteTransformerHostContract; + let makerToken: TestMintableERC20TokenContract; + let takerToken: TestMintableERC20TokenContract; + let takerFeeToken: TestMintableERC20TokenContract; + let singleProtocolFee: BigNumber; + + const GAS_PRICE = 1337; + + before(async () => { + [maker, feeRecipient] = await env.getAccountAddressesAsync(); + exchange = await TestFillQuoteTransformerExchangeContract.deployFrom0xArtifactAsync( + artifacts.TestFillQuoteTransformerExchange, + env.provider, + env.txDefaults, + artifacts, + ); + transformer = await FillQuoteTransformerContract.deployFrom0xArtifactAsync( + artifacts.FillQuoteTransformer, + env.provider, + env.txDefaults, + artifacts, + exchange.address, + ); + host = await TestFillQuoteTransformerHostContract.deployFrom0xArtifactAsync( + artifacts.TestFillQuoteTransformerHost, + env.provider, + { + ...env.txDefaults, + gasPrice: GAS_PRICE, + }, + artifacts, + ); + [makerToken, takerToken, takerFeeToken] = await Promise.all( + _.times(3, async () => + TestMintableERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.TestMintableERC20Token, + env.provider, + env.txDefaults, + artifacts, + ), + ), + ); + singleProtocolFee = (await exchange.protocolFeeMultiplier().callAsync()).times(GAS_PRICE); + }); + + type FilledOrder = Order & { filledTakerAssetAmount: BigNumber }; + + function createOrder(fields: Partial = {}): FilledOrder { + return { + chainId: 1, + exchangeAddress: exchange.address, + expirationTimeSeconds: ZERO_AMOUNT, + salt: ZERO_AMOUNT, + senderAddress: NULL_ADDRESS, + takerAddress: NULL_ADDRESS, + makerAddress: maker, + feeRecipientAddress: feeRecipient, + makerAssetAmount: getRandomInteger('0.1e18', '1e18'), + takerAssetAmount: getRandomInteger('0.1e18', '1e18'), + makerFee: ZERO_AMOUNT, + takerFee: getRandomInteger('0.001e18', '0.1e18'), + makerAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address), + takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address), + makerFeeAssetData: NULL_BYTES, + takerFeeAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address), + filledTakerAssetAmount: ZERO_AMOUNT, + ...fields, + }; + } + + interface QuoteFillResults { + makerAssetBought: BigNumber; + takerAssetSpent: BigNumber; + protocolFeePaid: BigNumber; + } + + const ZERO_QUOTE_FILL_RESULTS = { + makerAssetBought: ZERO_AMOUNT, + takerAssetSpent: ZERO_AMOUNT, + protocolFeePaid: ZERO_AMOUNT, + }; + + function getExpectedSellQuoteFillResults( + orders: FilledOrder[], + takerAssetFillAmount: BigNumber = constants.MAX_UINT256, + ): QuoteFillResults { + const qfr = { ...ZERO_QUOTE_FILL_RESULTS }; + for (const order of orders) { + if (qfr.takerAssetSpent.gte(takerAssetFillAmount)) { + break; + } + const singleFillAmount = BigNumber.min( + takerAssetFillAmount.minus(qfr.takerAssetSpent), + order.takerAssetAmount.minus(order.filledTakerAssetAmount), + ); + const fillRatio = singleFillAmount.div(order.takerAssetAmount); + qfr.takerAssetSpent = qfr.takerAssetSpent.plus(singleFillAmount); + qfr.protocolFeePaid = qfr.protocolFeePaid.plus(singleProtocolFee); + qfr.makerAssetBought = qfr.makerAssetBought.plus( + fillRatio.times(order.makerAssetAmount).integerValue(BigNumber.ROUND_DOWN), + ); + const takerFee = fillRatio.times(order.takerFee).integerValue(BigNumber.ROUND_DOWN); + if (order.takerAssetData === order.takerFeeAssetData) { + // Taker fee is in taker asset. + qfr.takerAssetSpent = qfr.takerAssetSpent.plus(takerFee); + } else if (order.makerAssetData === order.takerFeeAssetData) { + // Taker fee is in maker asset. + qfr.makerAssetBought = qfr.makerAssetBought.minus(takerFee); + } + } + return qfr; + } + + function getExpectedBuyQuoteFillResults( + orders: FilledOrder[], + makerAssetFillAmount: BigNumber = constants.MAX_UINT256, + ): QuoteFillResults { + const qfr = { ...ZERO_QUOTE_FILL_RESULTS }; + for (const order of orders) { + if (qfr.makerAssetBought.gte(makerAssetFillAmount)) { + break; + } + const filledMakerAssetAmount = order.filledTakerAssetAmount + .times(order.makerAssetAmount.div(order.takerAssetAmount)) + .integerValue(BigNumber.ROUND_DOWN); + const singleFillAmount = BigNumber.min( + makerAssetFillAmount.minus(qfr.makerAssetBought), + order.makerAssetAmount.minus(filledMakerAssetAmount), + ); + const fillRatio = singleFillAmount.div(order.makerAssetAmount); + qfr.takerAssetSpent = qfr.takerAssetSpent.plus( + fillRatio.times(order.takerAssetAmount).integerValue(BigNumber.ROUND_UP), + ); + qfr.protocolFeePaid = qfr.protocolFeePaid.plus(singleProtocolFee); + qfr.makerAssetBought = qfr.makerAssetBought.plus(singleFillAmount); + const takerFee = fillRatio.times(order.takerFee).integerValue(BigNumber.ROUND_UP); + if (order.takerAssetData === order.takerFeeAssetData) { + // Taker fee is in taker asset. + qfr.takerAssetSpent = qfr.takerAssetSpent.plus(takerFee); + } else if (order.makerAssetData === order.takerFeeAssetData) { + // Taker fee is in maker asset. + qfr.makerAssetBought = qfr.makerAssetBought.minus(takerFee); + } + } + return qfr; + } + + interface Balances { + makerAssetBalance: BigNumber; + takerAssetBalance: BigNumber; + takerFeeBalance: BigNumber; + protocolFeeBalance: BigNumber; + } + + const ZERO_BALANCES = { + makerAssetBalance: ZERO_AMOUNT, + takerAssetBalance: ZERO_AMOUNT, + takerFeeBalance: ZERO_AMOUNT, + protocolFeeBalance: ZERO_AMOUNT, + }; + + async function getBalancesAsync(owner: string): Promise { + const balances = { ...ZERO_BALANCES }; + [ + balances.makerAssetBalance, + balances.takerAssetBalance, + balances.takerFeeBalance, + balances.protocolFeeBalance, + ] = await Promise.all([ + makerToken.balanceOf(owner).callAsync(), + takerToken.balanceOf(owner).callAsync(), + takerFeeToken.balanceOf(owner).callAsync(), + env.web3Wrapper.getBalanceInWeiAsync(owner), + ]); + return balances; + } + + function assertBalances(actual: Balances, expected: Balances): void { + assertIntegerRoughlyEquals(actual.makerAssetBalance, expected.makerAssetBalance, 10, 'makerAssetBalance'); + assertIntegerRoughlyEquals(actual.takerAssetBalance, expected.takerAssetBalance, 10, 'takerAssetBalance'); + assertIntegerRoughlyEquals(actual.takerFeeBalance, expected.takerFeeBalance, 10, 'takerFeeBalance'); + assertIntegerRoughlyEquals(actual.protocolFeeBalance, expected.protocolFeeBalance, 10, 'protocolFeeBalance'); + } + + function encodeTransformData(fields: Partial = {}): string { + return encodeFillQuoteTransformerData({ + sellToken: takerToken.address, + buyToken: makerToken.address, + orders: [], + signatures: [], + maxOrderFillAmounts: [], + sellAmount: MAX_UINT256, + buyAmount: ZERO_AMOUNT, + ...fields, + }); + } + + function encodeExchangeBehavior( + filledTakerAssetAmount: Numberish = 0, + makerAssetMintRatio: Numberish = 1.0, + ): string { + return hexUtils.slice( + exchange + .encodeBehaviorData({ + filledTakerAssetAmount: new BigNumber(filledTakerAssetAmount), + makerAssetMintRatio: new BigNumber(makerAssetMintRatio).times('1e18').integerValue(), + }) + .getABIEncodedTransactionData(), + 4, + ); + } + + const ERC20_ASSET_PROXY_ID = '0xf47261b0'; + + describe('sell quotes', () => { + it('can fully sell to a single order quote', async () => { + const orders = _.times(1, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can fully sell to multi order quote', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can partially sell to single order quote', async () => { + const orders = _.times(1, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults( + orders, + getExpectedSellQuoteFillResults(orders).takerAssetSpent.dividedToIntegerBy(2), + ); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can partially sell to multi order quote and refund unused protocol fees', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders.slice(0, 2)); + const maxProtocolFees = singleProtocolFee.times(orders.length); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: maxProtocolFees }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + protocolFeeBalance: singleProtocolFee, + }); + }); + + it('can sell to multi order quote with a failing order', async () => { + const orders = _.times(3, () => createOrder()); + // First order will fail. + const validOrders = orders.slice(1); + const signatures = [NULL_BYTES, ...validOrders.map(() => encodeExchangeBehavior())]; + const qfr = getExpectedSellQuoteFillResults(validOrders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('succeeds if an order transfers too few maker tokens', async () => { + const mintScale = 0.5; + const orders = _.times(3, () => createOrder()); + // First order mints less than expected. + const signatures = [ + encodeExchangeBehavior(0, mintScale), + ...orders.slice(1).map(() => encodeExchangeBehavior()), + ]; + const qfr = getExpectedSellQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought + .minus(orders[0].makerAssetAmount.times(1 - mintScale)) + .integerValue(BigNumber.ROUND_DOWN), + }); + }); + + it('can fail if an order is partially filled', async () => { + const orders = _.times(3, () => createOrder()); + // First order is partially filled. + const filledOrder = { + ...orders[0], + filledTakerAssetAmount: orders[0].takerAssetAmount.dividedToIntegerBy(2), + }; + // First order is partially filled. + const signatures = [ + encodeExchangeBehavior(filledOrder.filledTakerAssetAmount), + ...orders.slice(1).map(() => encodeExchangeBehavior()), + ]; + const qfr = getExpectedSellQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.IncompleteFillSellQuoteError( + takerToken.address, + getExpectedSellQuoteFillResults([filledOrder, ...orders.slice(1)]).takerAssetSpent, + qfr.takerAssetSpent, + ), + ); + }); + + it('fails if not enough protocol fee provided', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid.minus(1) }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.InsufficientProtocolFeeError( + singleProtocolFee.minus(1), + singleProtocolFee, + ), + ); + }); + + it('can sell less than the taker token balance', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const takerTokenBalance = qfr.takerAssetSpent.times(1.01).integerValue(); + await host + .executeTransform( + transformer.address, + takerToken.address, + takerTokenBalance, + encodeTransformData({ + orders, + signatures, + sellAmount: qfr.takerAssetSpent, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + takerAssetBalance: qfr.takerAssetSpent.times(0.01).integerValue(), + }); + }); + + it('fails to sell more than the taker token balance', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const takerTokenBalance = qfr.takerAssetSpent.times(0.99).integerValue(); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + takerTokenBalance, + encodeTransformData({ + orders, + signatures, + sellAmount: qfr.takerAssetSpent, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.IncompleteFillSellQuoteError( + takerToken.address, + getExpectedSellQuoteFillResults(orders.slice(0, 2)).takerAssetSpent, + qfr.takerAssetSpent, + ), + ); + }); + + it('can fully sell to a single order with maker asset taker fees', async () => { + const orders = _.times(1, () => + createOrder({ + takerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address), + }), + ); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('fails if an order has a non-standard taker fee asset', async () => { + const BAD_ASSET_DATA = hexUtils.random(36); + const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.InvalidERC20AssetDataError(BAD_ASSET_DATA), + ); + }); + + it('fails if an order has a fee asset that is neither maker or taker asset', async () => { + const badToken = randomAddress(); + const BAD_ASSET_DATA = hexUtils.concat(ERC20_ASSET_PROXY_ID, hexUtils.leftPad(badToken)); + const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidTakerFeeTokenError(badToken)); + }); + + it('respects `maxOrderFillAmounts`', async () => { + const orders = _.times(2, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders.slice(1)); + const protocolFee = singleProtocolFee.times(2); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + // Skip the first order. + maxOrderFillAmounts: [ZERO_AMOUNT], + }), + ) + .awaitTransactionSuccessAsync({ value: protocolFee }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + }); + + describe('buy quotes', () => { + it('can fully buy from a single order quote', async () => { + const orders = _.times(1, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedBuyQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can fully buy from a multi order quote', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedBuyQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can partially buy from a single order quote', async () => { + const orders = _.times(1, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedBuyQuoteFillResults( + orders, + getExpectedBuyQuoteFillResults(orders).makerAssetBought.dividedToIntegerBy(2), + ); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('can partially buy from multi order quote and refund unused protocol fees', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedBuyQuoteFillResults(orders.slice(0, 2)); + const maxProtocolFees = singleProtocolFee.times(orders.length); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: maxProtocolFees }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + protocolFeeBalance: singleProtocolFee, + }); + }); + + it('can buy from multi order quote with a failing order', async () => { + const orders = _.times(3, () => createOrder()); + // First order will fail. + const validOrders = orders.slice(1); + const signatures = [NULL_BYTES, ...validOrders.map(() => encodeExchangeBehavior())]; + const qfr = getExpectedBuyQuoteFillResults(validOrders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('succeeds if an order transfers too many maker tokens', async () => { + const orders = _.times(2, () => createOrder()); + // First order will mint its tokens + the maker tokens of the second. + const mintScale = orders[1].makerAssetAmount.div(orders[0].makerAssetAmount.minus(1)).plus(1); + const signatures = [ + encodeExchangeBehavior(0, mintScale), + ...orders.slice(1).map(() => encodeExchangeBehavior()), + ]; + const qfr = getExpectedBuyQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: orders[0].makerAssetAmount.times(mintScale).integerValue(BigNumber.ROUND_DOWN), + takerAssetBalance: orders[1].takerAssetAmount.plus(orders[1].takerFee), + protocolFeeBalance: singleProtocolFee, + }); + }); + + it('fails to buy more than available in orders', async () => { + const orders = _.times(3, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedBuyQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought.plus(1), + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.IncompleteFillBuyQuoteError( + makerToken.address, + qfr.makerAssetBought, + qfr.makerAssetBought.plus(1), + ), + ); + }); + + it('can fully buy from a single order with maker asset taker fees', async () => { + const orders = _.times(1, () => + createOrder({ + takerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address), + }), + ); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedBuyQuoteFillResults(orders); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + + it('fails if an order has a non-standard taker fee asset', async () => { + const BAD_ASSET_DATA = hexUtils.random(36); + const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.InvalidERC20AssetDataError(BAD_ASSET_DATA), + ); + }); + + it('fails if an order has a fee asset that is neither maker or taker asset', async () => { + const badToken = randomAddress(); + const BAD_ASSET_DATA = hexUtils.concat(ERC20_ASSET_PROXY_ID, hexUtils.leftPad(badToken)); + const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders); + const tx = host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + }), + ) + .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); + return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidTakerFeeTokenError(badToken)); + }); + + it('respects `maxOrderFillAmounts`', async () => { + const orders = _.times(2, () => createOrder()); + const signatures = orders.map(() => encodeExchangeBehavior()); + const qfr = getExpectedSellQuoteFillResults(orders.slice(1)); + const protocolFee = singleProtocolFee.times(2); + await host + .executeTransform( + transformer.address, + takerToken.address, + qfr.takerAssetSpent, + encodeTransformData({ + orders, + signatures, + buyAmount: qfr.makerAssetBought, + // Skip the first order. + maxOrderFillAmounts: [ZERO_AMOUNT], + }), + ) + .awaitTransactionSuccessAsync({ value: protocolFee }); + assertBalances(await getBalancesAsync(host.address), { + ...ZERO_BALANCES, + makerAssetBalance: qfr.makerAssetBought, + }); + }); + }); +}); diff --git a/contracts/zero-ex/test/transformers/pay_taker_transformer.ts b/contracts/zero-ex/test/transformers/pay_taker_transformer.ts new file mode 100644 index 0000000000..215544bc87 --- /dev/null +++ b/contracts/zero-ex/test/transformers/pay_taker_transformer.ts @@ -0,0 +1,147 @@ +import { blockchainTests, constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils'; +import { BigNumber, hexUtils } from '@0x/utils'; +import * as _ from 'lodash'; + +import { ETH_TOKEN_ADDRESS } from '../../src/constants'; +import { encodePayTakerTransformerData } from '../../src/transformer_data_encoders'; +import { artifacts } from '../artifacts'; +import { PayTakerTransformerContract, TestMintableERC20TokenContract, TestTransformerHostContract } from '../wrappers'; + +const { MAX_UINT256, ZERO_AMOUNT } = constants; + +blockchainTests.resets('PayTakerTransformer', env => { + let caller: string; + const taker = randomAddress(); + let token: TestMintableERC20TokenContract; + let transformer: PayTakerTransformerContract; + let host: TestTransformerHostContract; + + before(async () => { + [caller] = await env.getAccountAddressesAsync(); + token = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.TestMintableERC20Token, + env.provider, + env.txDefaults, + artifacts, + ); + transformer = await PayTakerTransformerContract.deployFrom0xArtifactAsync( + artifacts.PayTakerTransformer, + env.provider, + env.txDefaults, + artifacts, + ); + host = await TestTransformerHostContract.deployFrom0xArtifactAsync( + artifacts.TestTransformerHost, + env.provider, + { ...env.txDefaults, from: caller }, + artifacts, + ); + }); + + interface Balances { + ethBalance: BigNumber; + tokenBalance: BigNumber; + } + + const ZERO_BALANCES = { + ethBalance: ZERO_AMOUNT, + tokenBalance: ZERO_AMOUNT, + }; + + async function getBalancesAsync(owner: string): Promise { + return { + ethBalance: await env.web3Wrapper.getBalanceInWeiAsync(owner), + tokenBalance: await token.balanceOf(owner).callAsync(), + }; + } + + async function mintHostTokensAsync(amount: BigNumber): Promise { + await token.mint(host.address, amount).awaitTransactionSuccessAsync(); + } + + async function sendEtherAsync(to: string, amount: BigNumber): Promise { + await env.web3Wrapper.awaitTransactionSuccessAsync( + await env.web3Wrapper.sendTransactionAsync({ + ...env.txDefaults, + to, + from: caller, + value: amount, + }), + ); + } + + it('can transfer a token and ETH', 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]); + await host + .rawExecuteTransform(transformer.address, hexUtils.random(), taker, data) + .awaitTransactionSuccessAsync(); + expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES); + expect(await getBalancesAsync(taker)).to.deep.eq({ + tokenBalance: amounts[0], + ethBalance: amounts[1], + }); + }); + + it('can transfer all of a token and ETH', async () => { + const amounts = _.times(2, () => getRandomInteger(1, '1e18')); + const data = encodePayTakerTransformerData({ + amounts: [MAX_UINT256, MAX_UINT256], + tokens: [token.address, ETH_TOKEN_ADDRESS], + }); + await mintHostTokensAsync(amounts[0]); + await sendEtherAsync(host.address, amounts[1]); + await host + .rawExecuteTransform(transformer.address, hexUtils.random(), taker, data) + .awaitTransactionSuccessAsync(); + expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES); + expect(await getBalancesAsync(taker)).to.deep.eq({ + tokenBalance: amounts[0], + ethBalance: amounts[1], + }); + }); + + it('can transfer all of a token and ETH (empty amounts)', 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]); + await host + .rawExecuteTransform(transformer.address, hexUtils.random(), taker, data) + .awaitTransactionSuccessAsync(); + expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES); + expect(await getBalancesAsync(taker)).to.deep.eq({ + tokenBalance: amounts[0], + ethBalance: amounts[1], + }); + }); + + it('can transfer less than the balance of a token and ETH', async () => { + const amounts = _.times(2, () => getRandomInteger(1, '1e18')); + const data = encodePayTakerTransformerData({ + amounts: amounts.map(a => a.dividedToIntegerBy(2)), + tokens: [token.address, ETH_TOKEN_ADDRESS], + }); + await mintHostTokensAsync(amounts[0]); + await sendEtherAsync(host.address, amounts[1]); + await host + .rawExecuteTransform(transformer.address, hexUtils.random(), taker, data) + .awaitTransactionSuccessAsync(); + expect(await getBalancesAsync(host.address)).to.deep.eq({ + tokenBalance: amounts[0].minus(amounts[0].dividedToIntegerBy(2)), + ethBalance: amounts[1].minus(amounts[1].dividedToIntegerBy(2)), + }); + expect(await getBalancesAsync(taker)).to.deep.eq({ + tokenBalance: amounts[0].dividedToIntegerBy(2), + ethBalance: amounts[1].dividedToIntegerBy(2), + }); + }); +}); diff --git a/contracts/zero-ex/test/transformers/weth_transformer_test.ts b/contracts/zero-ex/test/transformers/weth_transformer_test.ts new file mode 100644 index 0000000000..c4da09447d --- /dev/null +++ b/contracts/zero-ex/test/transformers/weth_transformer_test.ts @@ -0,0 +1,147 @@ +import { blockchainTests, constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils'; +import { BigNumber, ZeroExRevertErrors } from '@0x/utils'; +import * as _ from 'lodash'; + +import { ETH_TOKEN_ADDRESS } from '../../src/constants'; +import { encodeWethTransformerData } from '../../src/transformer_data_encoders'; +import { artifacts } from '../artifacts'; +import { TestWethContract, TestWethTransformerHostContract, WethTransformerContract } from '../wrappers'; + +const { MAX_UINT256, ZERO_AMOUNT } = constants; + +blockchainTests.resets('WethTransformer', env => { + let weth: TestWethContract; + let transformer: WethTransformerContract; + let host: TestWethTransformerHostContract; + + before(async () => { + weth = await TestWethContract.deployFrom0xArtifactAsync( + artifacts.TestWeth, + env.provider, + env.txDefaults, + artifacts, + ); + transformer = await WethTransformerContract.deployFrom0xArtifactAsync( + artifacts.WethTransformer, + env.provider, + env.txDefaults, + artifacts, + weth.address, + ); + host = await TestWethTransformerHostContract.deployFrom0xArtifactAsync( + artifacts.TestWethTransformerHost, + env.provider, + env.txDefaults, + artifacts, + weth.address, + ); + }); + + interface Balances { + ethBalance: BigNumber; + wethBalance: BigNumber; + } + + async function getHostBalancesAsync(): Promise { + return { + ethBalance: await env.web3Wrapper.getBalanceInWeiAsync(host.address), + wethBalance: await weth.balanceOf(host.address).callAsync(), + }; + } + + it('fails if the token is neither ETH or WETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount, + token: randomAddress(), + }); + const tx = host + .executeTransform(amount, transformer.address, data) + .awaitTransactionSuccessAsync({ value: amount }); + return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidTransformDataError(data)); + }); + + it('can unwrap WETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount, + token: weth.address, + }); + await host.executeTransform(amount, transformer.address, data).awaitTransactionSuccessAsync({ value: amount }); + expect(await getHostBalancesAsync()).to.deep.eq({ + ethBalance: amount, + wethBalance: ZERO_AMOUNT, + }); + }); + + it('can unwrap all WETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount: MAX_UINT256, + token: weth.address, + }); + await host.executeTransform(amount, transformer.address, data).awaitTransactionSuccessAsync({ value: amount }); + expect(await getHostBalancesAsync()).to.deep.eq({ + ethBalance: amount, + wethBalance: ZERO_AMOUNT, + }); + }); + + it('can unwrap some WETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount: amount.dividedToIntegerBy(2), + token: weth.address, + }); + await host.executeTransform(amount, transformer.address, data).awaitTransactionSuccessAsync({ value: amount }); + expect(await getHostBalancesAsync()).to.deep.eq({ + ethBalance: amount.dividedToIntegerBy(2), + wethBalance: amount.minus(amount.dividedToIntegerBy(2)), + }); + }); + + it('can wrap ETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount, + token: ETH_TOKEN_ADDRESS, + }); + await host + .executeTransform(ZERO_AMOUNT, transformer.address, data) + .awaitTransactionSuccessAsync({ value: amount }); + expect(await getHostBalancesAsync()).to.deep.eq({ + ethBalance: ZERO_AMOUNT, + wethBalance: amount, + }); + }); + + it('can wrap all ETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount: MAX_UINT256, + token: ETH_TOKEN_ADDRESS, + }); + await host + .executeTransform(ZERO_AMOUNT, transformer.address, data) + .awaitTransactionSuccessAsync({ value: amount }); + expect(await getHostBalancesAsync()).to.deep.eq({ + ethBalance: ZERO_AMOUNT, + wethBalance: amount, + }); + }); + + it('can wrap some ETH', async () => { + const amount = getRandomInteger(1, '1e18'); + const data = encodeWethTransformerData({ + amount: amount.dividedToIntegerBy(2), + token: ETH_TOKEN_ADDRESS, + }); + await host + .executeTransform(ZERO_AMOUNT, transformer.address, data) + .awaitTransactionSuccessAsync({ value: amount }); + expect(await getHostBalancesAsync()).to.deep.eq({ + ethBalance: amount.minus(amount.dividedToIntegerBy(2)), + wethBalance: amount.dividedToIntegerBy(2), + }); + }); +}); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 303c54a517..b674f9519c 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -5,12 +5,14 @@ */ export * from '../test/generated-wrappers/allowance_target'; export * from '../test/generated-wrappers/bootstrap'; +export * from '../test/generated-wrappers/fill_quote_transformer'; export * from '../test/generated-wrappers/fixin_common'; export * from '../test/generated-wrappers/flash_wallet'; export * from '../test/generated-wrappers/full_migration'; export * from '../test/generated-wrappers/i_allowance_target'; export * from '../test/generated-wrappers/i_bootstrap'; export * from '../test/generated-wrappers/i_erc20_transformer'; +export * from '../test/generated-wrappers/i_exchange'; export * from '../test/generated-wrappers/i_feature'; export * from '../test/generated-wrappers/i_flash_wallet'; export * from '../test/generated-wrappers/i_ownable'; @@ -49,8 +51,12 @@ export * from '../test/generated-wrappers/test_simple_function_registry_feature_ 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_host'; +export * from '../test/generated-wrappers/test_weth'; +export * from '../test/generated-wrappers/test_weth_transformer_host'; export * from '../test/generated-wrappers/test_zero_ex_feature'; export * from '../test/generated-wrappers/token_spender'; export * from '../test/generated-wrappers/token_spender_puppet'; export * from '../test/generated-wrappers/transform_erc20'; +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 a06888b4e8..07a67468db 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true }, "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], "files": [ + "generated-artifacts/FillQuoteTransformer.json", "generated-artifacts/FullMigration.json", "generated-artifacts/IAllowanceTarget.json", "generated-artifacts/IERC20Transformer.json", @@ -15,12 +16,14 @@ "generated-artifacts/ZeroEx.json", "test/generated-artifacts/AllowanceTarget.json", "test/generated-artifacts/Bootstrap.json", + "test/generated-artifacts/FillQuoteTransformer.json", "test/generated-artifacts/FixinCommon.json", "test/generated-artifacts/FlashWallet.json", "test/generated-artifacts/FullMigration.json", "test/generated-artifacts/IAllowanceTarget.json", "test/generated-artifacts/IBootstrap.json", "test/generated-artifacts/IERC20Transformer.json", + "test/generated-artifacts/IExchange.json", "test/generated-artifacts/IFeature.json", "test/generated-artifacts/IFlashWallet.json", "test/generated-artifacts/IOwnable.json", @@ -59,10 +62,14 @@ "test/generated-artifacts/TestTokenSpender.json", "test/generated-artifacts/TestTokenSpenderERC20Token.json", "test/generated-artifacts/TestTransformERC20.json", + "test/generated-artifacts/TestTransformerHost.json", + "test/generated-artifacts/TestWeth.json", + "test/generated-artifacts/TestWethTransformerHost.json", "test/generated-artifacts/TestZeroExFeature.json", "test/generated-artifacts/TokenSpender.json", "test/generated-artifacts/TokenSpenderPuppet.json", "test/generated-artifacts/TransformERC20.json", + "test/generated-artifacts/WethTransformer.json", "test/generated-artifacts/ZeroEx.json" ], "exclude": ["./deploy/solc/solc_bin"] From c610dd96f5d55941eea4f50c6ec3944efd710664 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 20 May 2020 02:08:11 -0400 Subject: [PATCH 13/19] `@0x/contracts-zero-ex`: Rebase, returning nonces in `transform()`. --- .../src/transformers/FillQuoteTransformer.sol | 18 +++++--- .../src/transformers/LibERC20Transformer.sol | 41 +++++++++++++++++ .../src/transformers/PayTakerTransformer.sol | 17 +++++-- .../src/transformers/WethTransformer.sol | 19 +++++--- .../test/TestFillQuoteTransformerHost.sol | 3 +- .../contracts/test/TestTransformerHost.sol | 6 +-- .../test/TestWethTransformerHost.sol | 3 +- contracts/zero-ex/package.json | 2 +- contracts/zero-ex/src/artifacts.ts | 10 ++--- contracts/zero-ex/src/nonce_utils.ts | 4 +- contracts/zero-ex/src/wrappers.ts | 4 +- contracts/zero-ex/test/artifacts.ts | 16 +++---- .../fill_quote_transformer_test.ts | 45 +++++++++++++++++-- ...ormer.ts => pay_taker_transformer_test.ts} | 17 ++++++- .../transformers/weth_transformer_test.ts | 13 ++++++ contracts/zero-ex/test/wrappers.ts | 3 ++ contracts/zero-ex/tsconfig.json | 5 +++ 17 files changed, 182 insertions(+), 44 deletions(-) rename contracts/zero-ex/test/transformers/{pay_taker_transformer.ts => pay_taker_transformer_test.ts} (89%) diff --git a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol index f004222ea8..565be0fb75 100644 --- a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol @@ -35,8 +35,6 @@ import "./LibERC20Transformer.sol"; contract FillQuoteTransformer is IERC20Transformer { - // solhint-disable indent,no-empty-blocks,no-unused-vars - /// @dev Transform data to ABI-encode and pass into `transform()`. struct TransformData { // The token being sold. @@ -76,6 +74,8 @@ contract FillQuoteTransformer is /// @dev The Exchange contract. IExchange public immutable exchange; + /// @dev The nonce of the deployer when deploying this contract. + uint256 public immutable deploymentNonce; /// @dev The ERC20Proxy address. address public immutable erc20Proxy; @@ -84,9 +84,13 @@ contract FillQuoteTransformer is using LibSafeMathV06 for uint256; using LibRichErrorsV06 for bytes; - constructor(IExchange exchange_) public { + /// @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_) public { exchange = exchange_; erc20Proxy = exchange_.getAssetProxy(ERC20_ASSET_PROXY_ID); + deploymentNonce = deploymentNonce_; } /// @dev Sell this contract's entire balance of of `sellToken` in exchange @@ -94,7 +98,9 @@ contract FillQuoteTransformer is /// to this call. `buyToken` and excess ETH will be transferred back to the caller. /// This function cannot be re-entered. /// @param data_ ABI-encoded `TransformData`. - /// @return success `TRANSFORMER_SUCCESS` on success. + /// @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. function transform( bytes32, // callDataHash, address payable, // taker, @@ -102,7 +108,7 @@ contract FillQuoteTransformer is ) external override - returns (bytes4 success) + returns (bytes memory rlpDeploymentNonce) { TransformData memory data = abi.decode(data_, (TransformData)); @@ -211,7 +217,7 @@ contract FillQuoteTransformer is ).rrevert(); } } - return LibERC20Transformer.TRANSFORMER_SUCCESS; + return LibERC20Transformer.rlpEncodeNonce(deploymentNonce); } /// @dev Try to sell up to `sellAmount` from an order. diff --git a/contracts/zero-ex/contracts/src/transformers/LibERC20Transformer.sol b/contracts/zero-ex/contracts/src/transformers/LibERC20Transformer.sol index 1753cc9c68..b0c08a0351 100644 --- a/contracts/zero-ex/contracts/src/transformers/LibERC20Transformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/LibERC20Transformer.sol @@ -70,4 +70,45 @@ library LibERC20Transformer { { return isTokenETH(token) ? owner.balance : 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) + internal + pure + returns (bytes memory rlpNonce) + { + if (nonce == 0) { + rlpNonce = new bytes(1); + rlpNonce[0] = 0x80; + } else if (nonce < 0x80) { + rlpNonce = new bytes(1); + rlpNonce[0] = byte(uint8(nonce)); + } else if (nonce <= 0xFF) { + rlpNonce = new bytes(2); + rlpNonce[0] = 0x81; + rlpNonce[1] = byte(uint8(nonce)); + } else if (nonce <= 0xFFFF) { + rlpNonce = new bytes(3); + rlpNonce[0] = 0x82; + rlpNonce[1] = byte(uint8((nonce & 0xFF00) >> 8)); + rlpNonce[2] = byte(uint8(nonce)); + } else if (nonce <= 0xFFFFFF) { + rlpNonce = new bytes(4); + rlpNonce[0] = 0x83; + rlpNonce[1] = byte(uint8((nonce & 0xFF0000) >> 16)); + rlpNonce[2] = byte(uint8((nonce & 0xFF00) >> 8)); + rlpNonce[3] = byte(uint8(nonce)); + } else if (nonce <= 0xFFFFFFFF) { + 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"); + } + } } diff --git a/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol index 4b668862c1..e5d59f7c7c 100644 --- a/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol @@ -45,10 +45,21 @@ contract PayTakerTransformer is using LibSafeMathV06 for uint256; using LibERC20Transformer for IERC20TokenV06; + /// @dev The nonce of the deployer when deploying this contract. + uint256 public immutable deploymentNonce; + + /// @dev Create this contract. + /// @param deploymentNonce_ The nonce of the deployer when deploying this contract. + constructor(uint256 deploymentNonce_) public { + deploymentNonce = deploymentNonce_; + } + /// @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 success `TRANSFORMER_SUCCESS` on success. + /// @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. function transform( bytes32, // callDataHash, address payable taker, @@ -56,7 +67,7 @@ contract PayTakerTransformer is ) external override - returns (bytes4 success) + returns (bytes memory rlpDeploymentNonce) { TransformData memory data = abi.decode(data_, (TransformData)); @@ -72,6 +83,6 @@ contract PayTakerTransformer is data.tokens[i].transformerTransfer(taker, amount); } } - return LibERC20Transformer.TRANSFORMER_SUCCESS; + return LibERC20Transformer.rlpEncodeNonce(deploymentNonce); } } diff --git a/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol b/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol index 003da666e5..69e9e4b33b 100644 --- a/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol @@ -40,24 +40,29 @@ contract WethTransformer is uint256 amount; } - // solhint-disable /// @dev The WETH contract address. IEtherTokenV06 public immutable weth; - // solhint-enable + /// @dev The nonce of the deployer when deploying this contract. + uint256 public immutable deploymentNonce; using LibRichErrorsV06 for bytes; using LibSafeMathV06 for uint256; using LibERC20Transformer for IERC20TokenV06; - /// @dev Construct the transformer and store the WETH address in an immutable. + /// @dev Create this contract. /// @param weth_ The weth token. - constructor(IEtherTokenV06 weth_) public { + /// @param deploymentNonce_ The nonce of the deployer when deploying this contract. + /// @dev Construct the transformer and store the WETH address in an immutable. + constructor(IEtherTokenV06 weth_, uint256 deploymentNonce_) public { weth = weth_; + deploymentNonce = deploymentNonce_; } /// @dev Wraps and unwraps WETH. /// @param data_ ABI-encoded `TransformData`, indicating which token to wrap/umwrap. - /// @return success `TRANSFORMER_SUCCESS` on success. + /// @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. function transform( bytes32, // callDataHash, address payable, // taker, @@ -65,7 +70,7 @@ contract WethTransformer is ) external override - returns (bytes4 success) + returns (bytes memory rlpDeploymentNonce) { TransformData memory data = abi.decode(data_, (TransformData)); if (!data.token.isTokenETH() && data.token != weth) { @@ -86,6 +91,6 @@ contract WethTransformer is weth.withdraw(amount); } } - return LibERC20Transformer.TRANSFORMER_SUCCESS; + return LibERC20Transformer.rlpEncodeNonce(deploymentNonce); } } diff --git a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol index 6b85acd2f5..3f9c196bd8 100644 --- a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol +++ b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol @@ -35,11 +35,12 @@ 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. - this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data); + return this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data); } } diff --git a/contracts/zero-ex/contracts/test/TestTransformerHost.sol b/contracts/zero-ex/contracts/test/TestTransformerHost.sol index 23f2fff764..283dc0e4fe 100644 --- a/contracts/zero-ex/contracts/test/TestTransformerHost.sol +++ b/contracts/zero-ex/contracts/test/TestTransformerHost.sol @@ -37,6 +37,7 @@ contract TestTransformerHost { bytes calldata data ) external + returns (bytes memory rlpDeploymentNonce) { (bool success, bytes memory resultData) = address(transformer).delegatecall(abi.encodeWithSelector( @@ -48,10 +49,7 @@ contract TestTransformerHost { if (!success) { resultData.rrevert(); } - require( - abi.decode(resultData, (bytes4)) == LibERC20Transformer.TRANSFORMER_SUCCESS, - "TestFillQuoteTransformerTaker/UNSUCCESSFUL_RESULT" - ); + assembly { return(add(resultData, 32), mload(resultData)) } } // solhint-disable diff --git a/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol b/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol index 3c0fd83999..fbadc6a9dd 100644 --- a/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol +++ b/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol @@ -43,11 +43,12 @@ 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. - this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data); + return this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data); } } diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 6a69f8eb38..fb07186d3e 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|FixinCommon|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Transformer|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|SimpleFunctionRegistry|TestCallTarget|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestZeroExFeature|TokenSpender|TransformERC20|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|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|WethTransformer|ZeroEx).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/src/artifacts.ts b/contracts/zero-ex/src/artifacts.ts index f68d20e56b..525dcc5da6 100644 --- a/contracts/zero-ex/src/artifacts.ts +++ b/contracts/zero-ex/src/artifacts.ts @@ -7,29 +7,29 @@ import { ContractArtifact } from 'ethereum-types'; import * as FillQuoteTransformer from '../generated-artifacts/FillQuoteTransformer.json'; import * as FullMigration from '../generated-artifacts/FullMigration.json'; +import * as IAllowanceTarget from '../generated-artifacts/IAllowanceTarget.json'; import * as IERC20Transformer from '../generated-artifacts/IERC20Transformer.json'; +import * as IFlashWallet from '../generated-artifacts/IFlashWallet.json'; import * as InitialMigration from '../generated-artifacts/InitialMigration.json'; import * as IOwnable from '../generated-artifacts/IOwnable.json'; import * as ISimpleFunctionRegistry from '../generated-artifacts/ISimpleFunctionRegistry.json'; import * as ITokenSpender from '../generated-artifacts/ITokenSpender.json'; import * as ITransformERC20 from '../generated-artifacts/ITransformERC20.json'; -import * as LibERC20Transformer from '../generated-artifacts/LibERC20Transformer.json'; import * as PayTakerTransformer from '../generated-artifacts/PayTakerTransformer.json'; -import * as Puppet from '../generated-artifacts/Puppet.json'; import * as WethTransformer from '../generated-artifacts/WethTransformer.json'; import * as ZeroEx from '../generated-artifacts/ZeroEx.json'; export const artifacts = { ZeroEx: ZeroEx as ContractArtifact, FullMigration: FullMigration as ContractArtifact, InitialMigration: InitialMigration as ContractArtifact, - Puppet: Puppet as ContractArtifact, + IFlashWallet: IFlashWallet as ContractArtifact, + IAllowanceTarget: IAllowanceTarget as ContractArtifact, IERC20Transformer: IERC20Transformer as ContractArtifact, IOwnable: IOwnable as ContractArtifact, ISimpleFunctionRegistry: ISimpleFunctionRegistry as ContractArtifact, ITokenSpender: ITokenSpender as ContractArtifact, ITransformERC20: ITransformERC20 as ContractArtifact, - LibERC20Transformer: LibERC20Transformer as ContractArtifact, + FillQuoteTransformer: FillQuoteTransformer as ContractArtifact, PayTakerTransformer: PayTakerTransformer as ContractArtifact, WethTransformer: WethTransformer as ContractArtifact, - FillQuoteTransformer: FillQuoteTransformer as ContractArtifact, }; diff --git a/contracts/zero-ex/src/nonce_utils.ts b/contracts/zero-ex/src/nonce_utils.ts index 9290fab280..157829fcb2 100644 --- a/contracts/zero-ex/src/nonce_utils.ts +++ b/contracts/zero-ex/src/nonce_utils.ts @@ -19,7 +19,7 @@ export function rlpEncodeNonce(nonce: number): string { } else if (nonce <= 0x7f) { return ethjs.bufferToHex(ethjs.toBuffer(nonce)); } else { - const rlpNonce = ethjs.bufferToHex(ethjs.toBuffer(nonce)); - return hexUtils.concat(rlpNonce.length + 0x80, rlpNonce); + const rlpNonce = ethjs.toBuffer(nonce); + return hexUtils.concat(rlpNonce.length + 0x80, ethjs.bufferToHex(rlpNonce)); } } diff --git a/contracts/zero-ex/src/wrappers.ts b/contracts/zero-ex/src/wrappers.ts index fdbf2d49c0..1778a3cc18 100644 --- a/contracts/zero-ex/src/wrappers.ts +++ b/contracts/zero-ex/src/wrappers.ts @@ -5,14 +5,14 @@ */ export * from '../generated-wrappers/fill_quote_transformer'; export * from '../generated-wrappers/full_migration'; +export * from '../generated-wrappers/i_allowance_target'; export * from '../generated-wrappers/i_erc20_transformer'; +export * from '../generated-wrappers/i_flash_wallet'; export * from '../generated-wrappers/i_ownable'; export * from '../generated-wrappers/i_simple_function_registry'; export * from '../generated-wrappers/i_token_spender'; export * from '../generated-wrappers/i_transform_erc20'; export * from '../generated-wrappers/initial_migration'; -export * from '../generated-wrappers/lib_erc20_transformer'; export * from '../generated-wrappers/pay_taker_transformer'; -export * from '../generated-wrappers/puppet'; export * from '../generated-wrappers/weth_transformer'; export * from '../generated-wrappers/zero_ex'; diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 26a642efb7..09476a1d5a 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -9,15 +9,16 @@ import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.js import * as Bootstrap from '../test/generated-artifacts/Bootstrap.json'; import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json'; import * as FixinCommon from '../test/generated-artifacts/FixinCommon.json'; +import * as FlashWallet from '../test/generated-artifacts/FlashWallet.json'; import * as FullMigration from '../test/generated-artifacts/FullMigration.json'; import * as IAllowanceTarget from '../test/generated-artifacts/IAllowanceTarget.json'; import * as IBootstrap from '../test/generated-artifacts/IBootstrap.json'; import * as IERC20Transformer from '../test/generated-artifacts/IERC20Transformer.json'; import * as IExchange from '../test/generated-artifacts/IExchange.json'; import * as IFeature from '../test/generated-artifacts/IFeature.json'; +import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json'; import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json'; import * as IOwnable from '../test/generated-artifacts/IOwnable.json'; -import * as IPuppet from '../test/generated-artifacts/IPuppet.json'; import * as ISimpleFunctionRegistry from '../test/generated-artifacts/ISimpleFunctionRegistry.json'; import * as ITestSimpleFunctionRegistryFeature from '../test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json'; import * as ITokenSpender from '../test/generated-artifacts/ITokenSpender.json'; @@ -30,7 +31,6 @@ import * as LibOwnableRichErrors from '../test/generated-artifacts/LibOwnableRic import * as LibOwnableStorage from '../test/generated-artifacts/LibOwnableStorage.json'; import * as LibProxyRichErrors from '../test/generated-artifacts/LibProxyRichErrors.json'; import * as LibProxyStorage from '../test/generated-artifacts/LibProxyStorage.json'; -import * as LibPuppetRichErrors from '../test/generated-artifacts/LibPuppetRichErrors.json'; import * as LibSimpleFunctionRegistryRichErrors from '../test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json'; import * as LibSimpleFunctionRegistryStorage from '../test/generated-artifacts/LibSimpleFunctionRegistryStorage.json'; import * as LibSpenderRichErrors from '../test/generated-artifacts/LibSpenderRichErrors.json'; @@ -38,10 +38,11 @@ import * as LibStorage from '../test/generated-artifacts/LibStorage.json'; import * as LibTokenSpenderStorage from '../test/generated-artifacts/LibTokenSpenderStorage.json'; import * as LibTransformERC20RichErrors from '../test/generated-artifacts/LibTransformERC20RichErrors.json'; import * as LibTransformERC20Storage from '../test/generated-artifacts/LibTransformERC20Storage.json'; +import * as LibWalletRichErrors from '../test/generated-artifacts/LibWalletRichErrors.json'; import * as Ownable from '../test/generated-artifacts/Ownable.json'; import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json'; -import * as Puppet from '../test/generated-artifacts/Puppet.json'; import * as SimpleFunctionRegistry from '../test/generated-artifacts/SimpleFunctionRegistry.json'; +import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; import * as TestFillQuoteTransformerExchange from '../test/generated-artifacts/TestFillQuoteTransformerExchange.json'; import * as TestFillQuoteTransformerHost from '../test/generated-artifacts/TestFillQuoteTransformerHost.json'; import * as TestFullMigration from '../test/generated-artifacts/TestFullMigration.json'; @@ -49,7 +50,6 @@ import * as TestInitialMigration from '../test/generated-artifacts/TestInitialMi import * as TestMigrator from '../test/generated-artifacts/TestMigrator.json'; import * as TestMintableERC20Token from '../test/generated-artifacts/TestMintableERC20Token.json'; import * as TestMintTokenERC20Transformer from '../test/generated-artifacts/TestMintTokenERC20Transformer.json'; -import * as TestPuppetTarget from '../test/generated-artifacts/TestPuppetTarget.json'; import * as TestSimpleFunctionRegistryFeatureImpl1 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl1.json'; import * as TestSimpleFunctionRegistryFeatureImpl2 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl2.json'; import * as TestTokenSpender from '../test/generated-artifacts/TestTokenSpender.json'; @@ -68,14 +68,14 @@ export const artifacts = { LibCommonRichErrors: LibCommonRichErrors as ContractArtifact, LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact, LibProxyRichErrors: LibProxyRichErrors as ContractArtifact, - LibPuppetRichErrors: LibPuppetRichErrors as ContractArtifact, LibSimpleFunctionRegistryRichErrors: LibSimpleFunctionRegistryRichErrors as ContractArtifact, LibSpenderRichErrors: LibSpenderRichErrors as ContractArtifact, LibTransformERC20RichErrors: LibTransformERC20RichErrors as ContractArtifact, + LibWalletRichErrors: LibWalletRichErrors as ContractArtifact, AllowanceTarget: AllowanceTarget as ContractArtifact, + FlashWallet: FlashWallet as ContractArtifact, IAllowanceTarget: IAllowanceTarget as ContractArtifact, - IPuppet: IPuppet as ContractArtifact, - Puppet: Puppet as ContractArtifact, + IFlashWallet: IFlashWallet as ContractArtifact, Bootstrap: Bootstrap as ContractArtifact, IBootstrap: IBootstrap as ContractArtifact, IFeature: IFeature as ContractArtifact, @@ -105,6 +105,7 @@ export const artifacts = { WethTransformer: WethTransformer as ContractArtifact, IExchange: IExchange as ContractArtifact, ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact, + TestCallTarget: TestCallTarget as ContractArtifact, TestFillQuoteTransformerExchange: TestFillQuoteTransformerExchange as ContractArtifact, TestFillQuoteTransformerHost: TestFillQuoteTransformerHost as ContractArtifact, TestFullMigration: TestFullMigration as ContractArtifact, @@ -112,7 +113,6 @@ export const artifacts = { TestMigrator: TestMigrator as ContractArtifact, TestMintTokenERC20Transformer: TestMintTokenERC20Transformer as ContractArtifact, TestMintableERC20Token: TestMintableERC20Token as ContractArtifact, - TestPuppetTarget: TestPuppetTarget as ContractArtifact, TestSimpleFunctionRegistryFeatureImpl1: TestSimpleFunctionRegistryFeatureImpl1 as ContractArtifact, TestSimpleFunctionRegistryFeatureImpl2: TestSimpleFunctionRegistryFeatureImpl2 as ContractArtifact, TestTokenSpender: TestTokenSpender as ContractArtifact, diff --git a/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts index 05e920a6b0..7f8c312e45 100644 --- a/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts +++ b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts @@ -12,6 +12,7 @@ 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 } from '../../src/transformer_data_encoders'; import { artifacts } from '../artifacts'; import { @@ -24,6 +25,7 @@ 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; @@ -50,6 +52,7 @@ blockchainTests.resets('FillQuoteTransformer', env => { env.txDefaults, artifacts, exchange.address, + new BigNumber(deploymentNonce), ); host = await TestFillQuoteTransformerHostContract.deployFrom0xArtifactAsync( artifacts.TestFillQuoteTransformerHost, @@ -578,6 +581,24 @@ 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', () => { @@ -782,7 +803,7 @@ blockchainTests.resets('FillQuoteTransformer', env => { const BAD_ASSET_DATA = hexUtils.random(36); const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders); + const qfr = getExpectedBuyQuoteFillResults(orders); const tx = host .executeTransform( transformer.address, @@ -805,7 +826,7 @@ blockchainTests.resets('FillQuoteTransformer', env => { const BAD_ASSET_DATA = hexUtils.concat(ERC20_ASSET_PROXY_ID, hexUtils.leftPad(badToken)); const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA })); const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders); + const qfr = getExpectedBuyQuoteFillResults(orders); const tx = host .executeTransform( transformer.address, @@ -824,7 +845,7 @@ blockchainTests.resets('FillQuoteTransformer', env => { it('respects `maxOrderFillAmounts`', async () => { const orders = _.times(2, () => createOrder()); const signatures = orders.map(() => encodeExchangeBehavior()); - const qfr = getExpectedSellQuoteFillResults(orders.slice(1)); + const qfr = getExpectedBuyQuoteFillResults(orders.slice(1)); const protocolFee = singleProtocolFee.times(2); await host .executeTransform( @@ -845,5 +866,23 @@ 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, + }), + ) + .callAsync({ value: qfr.protocolFeePaid }); + expect(r).to.eq(rlpEncodeNonce(deploymentNonce)); + }); }); }); diff --git a/contracts/zero-ex/test/transformers/pay_taker_transformer.ts b/contracts/zero-ex/test/transformers/pay_taker_transformer_test.ts similarity index 89% rename from contracts/zero-ex/test/transformers/pay_taker_transformer.ts rename to contracts/zero-ex/test/transformers/pay_taker_transformer_test.ts index 215544bc87..3117f19a3e 100644 --- a/contracts/zero-ex/test/transformers/pay_taker_transformer.ts +++ b/contracts/zero-ex/test/transformers/pay_taker_transformer_test.ts @@ -3,6 +3,7 @@ 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'; @@ -10,8 +11,9 @@ import { PayTakerTransformerContract, TestMintableERC20TokenContract, TestTransf const { MAX_UINT256, ZERO_AMOUNT } = constants; blockchainTests.resets('PayTakerTransformer', env => { - let caller: string; const taker = randomAddress(); + const deploymentNonce = _.random(0, 0xffffffff); + let caller: string; let token: TestMintableERC20TokenContract; let transformer: PayTakerTransformerContract; let host: TestTransformerHostContract; @@ -29,6 +31,7 @@ blockchainTests.resets('PayTakerTransformer', env => { env.provider, env.txDefaults, artifacts, + new BigNumber(deploymentNonce), ); host = await TestTransformerHostContract.deployFrom0xArtifactAsync( artifacts.TestTransformerHost, @@ -144,4 +147,16 @@ 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)); + }); }); diff --git a/contracts/zero-ex/test/transformers/weth_transformer_test.ts b/contracts/zero-ex/test/transformers/weth_transformer_test.ts index c4da09447d..e5c1cc8c26 100644 --- a/contracts/zero-ex/test/transformers/weth_transformer_test.ts +++ b/contracts/zero-ex/test/transformers/weth_transformer_test.ts @@ -3,6 +3,7 @@ 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'; @@ -10,6 +11,7 @@ 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; @@ -27,6 +29,7 @@ blockchainTests.resets('WethTransformer', env => { env.txDefaults, artifacts, weth.address, + new BigNumber(deploymentNonce), ); host = await TestWethTransformerHostContract.deployFrom0xArtifactAsync( artifacts.TestWethTransformerHost, @@ -144,4 +147,14 @@ 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)); + }); }); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index b674f9519c..140573cfdf 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -39,8 +39,11 @@ export * from '../test/generated-wrappers/lib_transform_erc20_rich_errors'; export * from '../test/generated-wrappers/lib_transform_erc20_storage'; export * from '../test/generated-wrappers/lib_wallet_rich_errors'; export * from '../test/generated-wrappers/ownable'; +export * from '../test/generated-wrappers/pay_taker_transformer'; export * from '../test/generated-wrappers/simple_function_registry'; export * from '../test/generated-wrappers/test_call_target'; +export * from '../test/generated-wrappers/test_fill_quote_transformer_exchange'; +export * from '../test/generated-wrappers/test_fill_quote_transformer_host'; export * from '../test/generated-wrappers/test_full_migration'; export * from '../test/generated-wrappers/test_initial_migration'; export * from '../test/generated-wrappers/test_migrator'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 07a67468db..288e41bd47 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -13,6 +13,8 @@ "generated-artifacts/ITokenSpender.json", "generated-artifacts/ITransformERC20.json", "generated-artifacts/InitialMigration.json", + "generated-artifacts/PayTakerTransformer.json", + "generated-artifacts/WethTransformer.json", "generated-artifacts/ZeroEx.json", "test/generated-artifacts/AllowanceTarget.json", "test/generated-artifacts/Bootstrap.json", @@ -50,8 +52,11 @@ "test/generated-artifacts/LibTransformERC20Storage.json", "test/generated-artifacts/LibWalletRichErrors.json", "test/generated-artifacts/Ownable.json", + "test/generated-artifacts/PayTakerTransformer.json", "test/generated-artifacts/SimpleFunctionRegistry.json", "test/generated-artifacts/TestCallTarget.json", + "test/generated-artifacts/TestFillQuoteTransformerExchange.json", + "test/generated-artifacts/TestFillQuoteTransformerHost.json", "test/generated-artifacts/TestFullMigration.json", "test/generated-artifacts/TestInitialMigration.json", "test/generated-artifacts/TestMigrator.json", From e1d213d1a3bd203f0ea29ea1594757ea3b5ef452 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 20 May 2020 14:41:44 -0400 Subject: [PATCH 14/19] `@0x/utils`: Add more transformer revert errors. --- .../zero-ex/transform_erc20_revert_errors.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/utils/src/revert_errors/zero-ex/transform_erc20_revert_errors.ts b/packages/utils/src/revert_errors/zero-ex/transform_erc20_revert_errors.ts index 1502121369..7cd2d2b4f8 100644 --- a/packages/utils/src/revert_errors/zero-ex/transform_erc20_revert_errors.ts +++ b/packages/utils/src/revert_errors/zero-ex/transform_erc20_revert_errors.ts @@ -53,6 +53,28 @@ export class InvalidRLPNonceError extends RevertError { } } +export class OnlyCallableByDeployerError extends RevertError { + constructor(caller?: string, deployer?: string) { + super('OnlyCallableByDeployerError', 'OnlyCallableByDeployerError(address caller, address deployer)', { + caller, + deployer, + }); + } +} + +export class InvalidExecutionContextError extends RevertError { + constructor(actualContext?: string, expectedContext?: string) { + super( + 'InvalidExecutionContextError', + 'InvalidExecutionContextError(address actualContext, address expectedContext)', + { + actualContext, + expectedContext, + }, + ); + } +} + export class InvalidTransformDataError extends RevertError { constructor(transformData?: string) { super('InvalidTransformDataError', 'InvalidTransformDataError(bytes transformData)', { @@ -163,6 +185,8 @@ const types = [ InvalidTokenReceivedError, InvalidTransformDataError, InvalidTakerFeeTokenError, + OnlyCallableByDeployerError, + InvalidExecutionContextError, ]; // Register the types we've defined. From 28402ff7d89898c8b13bf48f0cf0a56330f0ce53 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 20 May 2020 14:42:09 -0400 Subject: [PATCH 15/19] `@0x/contracts-zero-ex`: Add self-destructing to transformers --- .../errors/LibTransformERC20RichErrors.sol | 30 +++++++ .../src/transformers/FillQuoteTransformer.sol | 14 ++-- .../src/transformers/PayTakerTransformer.sol | 19 ++--- .../src/transformers/Transformer.sol | 80 +++++++++++++++++++ .../src/transformers/WethTransformer.sol | 14 ++-- .../contracts/test/TestDelegateCaller.sol | 37 +++++++++ .../contracts/test/TestTransformerBase.sol | 53 ++++++++++++ .../contracts/test/TestTransformerHost.sol | 2 +- contracts/zero-ex/package.json | 2 +- contracts/zero-ex/test/artifacts.ts | 6 ++ .../transformers/transformer_base_test.ts | 68 ++++++++++++++++ contracts/zero-ex/test/wrappers.ts | 3 + contracts/zero-ex/tsconfig.json | 3 + 13 files changed, 306 insertions(+), 25 deletions(-) create mode 100644 contracts/zero-ex/contracts/src/transformers/Transformer.sol create mode 100644 contracts/zero-ex/contracts/test/TestDelegateCaller.sol create mode 100644 contracts/zero-ex/contracts/test/TestTransformerBase.sol create mode 100644 contracts/zero-ex/test/transformers/transformer_base_test.ts diff --git a/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol index 4ce8f16687..cc1352eac4 100644 --- a/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol +++ b/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol @@ -100,6 +100,36 @@ library LibTransformERC20RichErrors { // Common Transformer errors /////////////////////////////////////////////// + function OnlyCallableByDeployerError( + address caller, + address deployer + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("OnlyCallableByDeployerError(address,address)")), + caller, + deployer + ); + } + + function InvalidExecutionContextError( + address actualContext, + address expectedContext + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("InvalidExecutionContextError(address,address)")), + actualContext, + expectedContext + ); + } + function InvalidTransformDataError( bytes memory transformData ) diff --git a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol index 565be0fb75..39604ce763 100644 --- a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol @@ -27,13 +27,13 @@ import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol"; import "../errors/LibTransformERC20RichErrors.sol"; import "../vendor/v3/IExchange.sol"; -import "./IERC20Transformer.sol"; +import "./Transformer.sol"; import "./LibERC20Transformer.sol"; /// @dev A transformer that fills an ERC20 market sell/buy quote. contract FillQuoteTransformer is - IERC20Transformer + Transformer { /// @dev Transform data to ABI-encode and pass into `transform()`. struct TransformData { @@ -74,8 +74,6 @@ contract FillQuoteTransformer is /// @dev The Exchange contract. IExchange public immutable exchange; - /// @dev The nonce of the deployer when deploying this contract. - uint256 public immutable deploymentNonce; /// @dev The ERC20Proxy address. address public immutable erc20Proxy; @@ -87,10 +85,12 @@ 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_) public { + constructor(IExchange exchange_, uint256 deploymentNonce_) + public + Transformer(deploymentNonce_) + { exchange = exchange_; erc20Proxy = exchange_.getAssetProxy(ERC20_ASSET_PROXY_ID); - deploymentNonce = deploymentNonce_; } /// @dev Sell this contract's entire balance of of `sellToken` in exchange @@ -217,7 +217,7 @@ contract FillQuoteTransformer is ).rrevert(); } } - return LibERC20Transformer.rlpEncodeNonce(deploymentNonce); + return _getRLPEncodedDeploymentNonce(); } /// @dev Try to sell up to `sellAmount` from an order. diff --git a/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol index e5d59f7c7c..7c09c9ddd4 100644 --- a/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol @@ -24,14 +24,16 @@ import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "../errors/LibTransformERC20RichErrors.sol"; -import "./IERC20Transformer.sol"; +import "./Transformer.sol"; import "./LibERC20Transformer.sol"; /// @dev A transformer that transfers tokens to the taker. contract PayTakerTransformer is - IERC20Transformer + Transformer { + // solhint-disable no-empty-blocks + /// @dev Transform data to ABI-encode and pass into `transform()`. struct TransformData { // The tokens to transfer to the taker. @@ -45,14 +47,13 @@ contract PayTakerTransformer is using LibSafeMathV06 for uint256; using LibERC20Transformer for IERC20TokenV06; - /// @dev The nonce of the deployer when deploying this contract. - uint256 public immutable deploymentNonce; - /// @dev Create this contract. /// @param deploymentNonce_ The nonce of the deployer when deploying this contract. - constructor(uint256 deploymentNonce_) public { - deploymentNonce = deploymentNonce_; - } + /// @dev Construct the transformer and store the WETH address in an immutable. + constructor(uint256 deploymentNonce_) + public + Transformer(deploymentNonce_) + {} /// @dev Forwards tokens to the taker. /// @param taker The taker address (caller of `TransformERC20.transformERC20()`). @@ -83,6 +84,6 @@ contract PayTakerTransformer is data.tokens[i].transformerTransfer(taker, amount); } } - return LibERC20Transformer.rlpEncodeNonce(deploymentNonce); + return _getRLPEncodedDeploymentNonce(); } } diff --git a/contracts/zero-ex/contracts/src/transformers/Transformer.sol b/contracts/zero-ex/contracts/src/transformers/Transformer.sol new file mode 100644 index 0000000000..ec9a942d64 --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/Transformer.sol @@ -0,0 +1,80 @@ +/* + + 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/errors/LibRichErrorsV06.sol"; +import "../errors/LibTransformERC20RichErrors.sol"; +import "./IERC20Transformer.sol"; +import "./LibERC20Transformer.sol"; + + +/// @dev Abstract base class for transformers. +abstract contract Transformer is + IERC20Transformer +{ + using LibRichErrorsV06 for bytes; + + /// @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_; + deployer = msg.sender; + _implementation = address(this); + } + + /// @dev Destruct this contract. Only callable by the deployer and will not + /// succeed in the context of a delegatecall (from another contract). + /// @param ethRecipient The recipient of ETH held in this contract. + function die(address payable ethRecipient) + external + virtual + { + // Only the deployer can call this. + if (msg.sender != deployer) { + LibTransformERC20RichErrors + .OnlyCallableByDeployerError(msg.sender, deployer) + .rrevert(); + } + // Must be executing our own context. + if (address(this) != _implementation) { + LibTransformERC20RichErrors + .InvalidExecutionContextError(address(this), _implementation) + .rrevert(); + } + 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); + } +} diff --git a/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol b/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol index 69e9e4b33b..35678dbf17 100644 --- a/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol @@ -23,13 +23,13 @@ import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; import "../errors/LibTransformERC20RichErrors.sol"; -import "./IERC20Transformer.sol"; +import "./Transformer.sol"; import "./LibERC20Transformer.sol"; /// @dev A transformer that wraps or unwraps WETH. contract WethTransformer is - IERC20Transformer + Transformer { /// @dev Transform data to ABI-encode and pass into `transform()`. struct TransformData { @@ -42,8 +42,6 @@ contract WethTransformer is /// @dev The WETH contract address. IEtherTokenV06 public immutable weth; - /// @dev The nonce of the deployer when deploying this contract. - uint256 public immutable deploymentNonce; using LibRichErrorsV06 for bytes; using LibSafeMathV06 for uint256; @@ -53,9 +51,11 @@ contract WethTransformer is /// @param weth_ The weth token. /// @param deploymentNonce_ The nonce of the deployer when deploying this contract. /// @dev Construct the transformer and store the WETH address in an immutable. - constructor(IEtherTokenV06 weth_, uint256 deploymentNonce_) public { + constructor(IEtherTokenV06 weth_, uint256 deploymentNonce_) + public + Transformer(deploymentNonce_) + { weth = weth_; - deploymentNonce = deploymentNonce_; } /// @dev Wraps and unwraps WETH. @@ -91,6 +91,6 @@ contract WethTransformer is weth.withdraw(amount); } } - return LibERC20Transformer.rlpEncodeNonce(deploymentNonce); + return _getRLPEncodedDeploymentNonce(); } } diff --git a/contracts/zero-ex/contracts/test/TestDelegateCaller.sol b/contracts/zero-ex/contracts/test/TestDelegateCaller.sol new file mode 100644 index 0000000000..0fcbf3aef5 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestDelegateCaller.sol @@ -0,0 +1,37 @@ +/* + + 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; + + + +contract TestDelegateCaller { + function executeDelegateCall( + address target, + bytes calldata callData + ) + external + { + (bool success, bytes memory resultData) = target.delegatecall(callData); + if (!success) { + assembly { revert(add(resultData, 32), mload(resultData)) } + } + assembly { return(add(resultData, 32), mload(resultData)) } + } +} diff --git a/contracts/zero-ex/contracts/test/TestTransformerBase.sol b/contracts/zero-ex/contracts/test/TestTransformerBase.sol new file mode 100644 index 0000000000..f4ae45e07d --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestTransformerBase.sol @@ -0,0 +1,53 @@ +/* + + 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/Transformer.sol"; + + +contract TestTransformerBase is + Transformer +{ + // solhint-disable no-empty-blocks + constructor(uint256 deploymentNonce_) + public + Transformer(deploymentNonce_) + {} + + function transform( + bytes32, + address payable, + bytes calldata + ) + external + override + returns (bytes memory rlpDeploymentNonce) + { + return hex""; + } + + function getRLPEncodedDeploymentNonce() + external + view + returns (bytes memory) + { + return _getRLPEncodedDeploymentNonce(); + } +} diff --git a/contracts/zero-ex/contracts/test/TestTransformerHost.sol b/contracts/zero-ex/contracts/test/TestTransformerHost.sol index 283dc0e4fe..57344ae885 100644 --- a/contracts/zero-ex/contracts/test/TestTransformerHost.sol +++ b/contracts/zero-ex/contracts/test/TestTransformerHost.sol @@ -37,7 +37,7 @@ contract TestTransformerHost { bytes calldata data ) external - returns (bytes memory rlpDeploymentNonce) + returns (bytes memory) { (bool success, bytes memory resultData) = address(transformer).delegatecall(abi.encodeWithSelector( diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index fb07186d3e..d94206f4e9 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|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|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|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|WethTransformer|ZeroEx).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 09476a1d5a..4774071dca 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -43,6 +43,7 @@ import * as Ownable from '../test/generated-artifacts/Ownable.json'; import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json'; import * as SimpleFunctionRegistry from '../test/generated-artifacts/SimpleFunctionRegistry.json'; import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; +import * as TestDelegateCaller from '../test/generated-artifacts/TestDelegateCaller.json'; import * as TestFillQuoteTransformerExchange from '../test/generated-artifacts/TestFillQuoteTransformerExchange.json'; import * as TestFillQuoteTransformerHost from '../test/generated-artifacts/TestFillQuoteTransformerHost.json'; import * as TestFullMigration from '../test/generated-artifacts/TestFullMigration.json'; @@ -54,12 +55,14 @@ import * as TestSimpleFunctionRegistryFeatureImpl1 from '../test/generated-artif import * as TestSimpleFunctionRegistryFeatureImpl2 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl2.json'; import * as TestTokenSpender from '../test/generated-artifacts/TestTokenSpender.json'; 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 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'; import * as TestZeroExFeature from '../test/generated-artifacts/TestZeroExFeature.json'; 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 WethTransformer from '../test/generated-artifacts/WethTransformer.json'; import * as ZeroEx from '../test/generated-artifacts/ZeroEx.json'; @@ -102,10 +105,12 @@ export const artifacts = { IERC20Transformer: IERC20Transformer as ContractArtifact, LibERC20Transformer: LibERC20Transformer as ContractArtifact, PayTakerTransformer: PayTakerTransformer as ContractArtifact, + Transformer: Transformer as ContractArtifact, WethTransformer: WethTransformer as ContractArtifact, IExchange: IExchange as ContractArtifact, ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact, TestCallTarget: TestCallTarget as ContractArtifact, + TestDelegateCaller: TestDelegateCaller as ContractArtifact, TestFillQuoteTransformerExchange: TestFillQuoteTransformerExchange as ContractArtifact, TestFillQuoteTransformerHost: TestFillQuoteTransformerHost as ContractArtifact, TestFullMigration: TestFullMigration as ContractArtifact, @@ -118,6 +123,7 @@ export const artifacts = { TestTokenSpender: TestTokenSpender as ContractArtifact, TestTokenSpenderERC20Token: TestTokenSpenderERC20Token as ContractArtifact, TestTransformERC20: TestTransformERC20 as ContractArtifact, + TestTransformerBase: TestTransformerBase as ContractArtifact, TestTransformerHost: TestTransformerHost as ContractArtifact, TestWeth: TestWeth as ContractArtifact, TestWethTransformerHost: TestWethTransformerHost as ContractArtifact, diff --git a/contracts/zero-ex/test/transformers/transformer_base_test.ts b/contracts/zero-ex/test/transformers/transformer_base_test.ts new file mode 100644 index 0000000000..9f8324d74a --- /dev/null +++ b/contracts/zero-ex/test/transformers/transformer_base_test.ts @@ -0,0 +1,68 @@ +import { blockchainTests, constants, expect, randomAddress } from '@0x/contracts-test-utils'; +import { BigNumber, 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; + + before(async () => { + [deployer] = await env.getAccountAddressesAsync(); + delegateCaller = await TestDelegateCallerContract.deployFrom0xArtifactAsync( + artifacts.TestDelegateCaller, + env.provider, + env.txDefaults, + artifacts, + ); + transformer = await TestTransformerBaseContract.deployFrom0xArtifactAsync( + artifacts.TestTransformerBase, + env.provider, + { + ...env.txDefaults, + 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(); + const tx = transformer.die(randomAddress()).callAsync({ from: notDeployer }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.OnlyCallableByDeployerError(notDeployer, deployer), + ); + }); + + it('cannot be called outside of its own context', async () => { + const callData = transformer.die(randomAddress()).getABIEncodedTransactionData(); + const tx = delegateCaller.executeDelegateCall(transformer.address, callData).callAsync({ from: deployer }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.InvalidExecutionContextError( + delegateCaller.address, + transformer.address, + ), + ); + }); + + it('destroys the transformer', async () => { + await transformer.die(randomAddress()).awaitTransactionSuccessAsync({ from: deployer }); + const code = await env.web3Wrapper.getContractCodeAsync(transformer.address); + return expect(code).to.eq(constants.NULL_BYTES); + }); + }); +}); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 140573cfdf..94390b6387 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -42,6 +42,7 @@ export * from '../test/generated-wrappers/ownable'; export * from '../test/generated-wrappers/pay_taker_transformer'; export * from '../test/generated-wrappers/simple_function_registry'; export * from '../test/generated-wrappers/test_call_target'; +export * from '../test/generated-wrappers/test_delegate_caller'; export * from '../test/generated-wrappers/test_fill_quote_transformer_exchange'; export * from '../test/generated-wrappers/test_fill_quote_transformer_host'; export * from '../test/generated-wrappers/test_full_migration'; @@ -54,6 +55,7 @@ export * from '../test/generated-wrappers/test_simple_function_registry_feature_ 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_host'; export * from '../test/generated-wrappers/test_weth'; export * from '../test/generated-wrappers/test_weth_transformer_host'; @@ -61,5 +63,6 @@ export * from '../test/generated-wrappers/test_zero_ex_feature'; export * from '../test/generated-wrappers/token_spender'; export * from '../test/generated-wrappers/token_spender_puppet'; export * from '../test/generated-wrappers/transform_erc20'; +export * from '../test/generated-wrappers/transformer'; 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 288e41bd47..637fe2eece 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -55,6 +55,7 @@ "test/generated-artifacts/PayTakerTransformer.json", "test/generated-artifacts/SimpleFunctionRegistry.json", "test/generated-artifacts/TestCallTarget.json", + "test/generated-artifacts/TestDelegateCaller.json", "test/generated-artifacts/TestFillQuoteTransformerExchange.json", "test/generated-artifacts/TestFillQuoteTransformerHost.json", "test/generated-artifacts/TestFullMigration.json", @@ -67,6 +68,7 @@ "test/generated-artifacts/TestTokenSpender.json", "test/generated-artifacts/TestTokenSpenderERC20Token.json", "test/generated-artifacts/TestTransformERC20.json", + "test/generated-artifacts/TestTransformerBase.json", "test/generated-artifacts/TestTransformerHost.json", "test/generated-artifacts/TestWeth.json", "test/generated-artifacts/TestWethTransformerHost.json", @@ -74,6 +76,7 @@ "test/generated-artifacts/TokenSpender.json", "test/generated-artifacts/TokenSpenderPuppet.json", "test/generated-artifacts/TransformERC20.json", + "test/generated-artifacts/Transformer.json", "test/generated-artifacts/WethTransformer.json", "test/generated-artifacts/ZeroEx.json" ], From a7ce72cae02204e49267f71d7808af919b822f8e Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 20 May 2020 23:26:05 -0400 Subject: [PATCH 16/19] `@0x/contracts-zero-ex`: Rebase against development --- contracts/zero-ex/test/wrappers.ts | 2 -- contracts/zero-ex/tsconfig.json | 2 -- 2 files changed, 4 deletions(-) diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 94390b6387..561f332dab 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -19,7 +19,6 @@ export * from '../test/generated-wrappers/i_ownable'; export * from '../test/generated-wrappers/i_simple_function_registry'; export * from '../test/generated-wrappers/i_test_simple_function_registry_feature'; export * from '../test/generated-wrappers/i_token_spender'; -export * from '../test/generated-wrappers/i_token_spender_puppet'; export * from '../test/generated-wrappers/i_transform_erc20'; export * from '../test/generated-wrappers/initial_migration'; export * from '../test/generated-wrappers/lib_bootstrap'; @@ -61,7 +60,6 @@ export * from '../test/generated-wrappers/test_weth'; export * from '../test/generated-wrappers/test_weth_transformer_host'; export * from '../test/generated-wrappers/test_zero_ex_feature'; export * from '../test/generated-wrappers/token_spender'; -export * from '../test/generated-wrappers/token_spender_puppet'; export * from '../test/generated-wrappers/transform_erc20'; export * from '../test/generated-wrappers/transformer'; export * from '../test/generated-wrappers/weth_transformer'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 637fe2eece..17110f5474 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -32,7 +32,6 @@ "test/generated-artifacts/ISimpleFunctionRegistry.json", "test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json", "test/generated-artifacts/ITokenSpender.json", - "test/generated-artifacts/ITokenSpenderPuppet.json", "test/generated-artifacts/ITransformERC20.json", "test/generated-artifacts/InitialMigration.json", "test/generated-artifacts/LibBootstrap.json", @@ -74,7 +73,6 @@ "test/generated-artifacts/TestWethTransformerHost.json", "test/generated-artifacts/TestZeroExFeature.json", "test/generated-artifacts/TokenSpender.json", - "test/generated-artifacts/TokenSpenderPuppet.json", "test/generated-artifacts/TransformERC20.json", "test/generated-artifacts/Transformer.json", "test/generated-artifacts/WethTransformer.json", From bf844098392cfd08af419b01b25331b50ada286d Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 28 May 2020 11:55:07 -0400 Subject: [PATCH 17/19] `@0x/utils`: Update `InvalidTransformDataError`. --- .../zero-ex/transform_erc20_revert_errors.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/revert_errors/zero-ex/transform_erc20_revert_errors.ts b/packages/utils/src/revert_errors/zero-ex/transform_erc20_revert_errors.ts index 7cd2d2b4f8..6852d3b505 100644 --- a/packages/utils/src/revert_errors/zero-ex/transform_erc20_revert_errors.ts +++ b/packages/utils/src/revert_errors/zero-ex/transform_erc20_revert_errors.ts @@ -75,9 +75,15 @@ export class InvalidExecutionContextError extends RevertError { } } +export enum InvalidTransformDataErrorCode { + InvalidTokens, + InvalidArrayLength, +} + export class InvalidTransformDataError extends RevertError { - constructor(transformData?: string) { - super('InvalidTransformDataError', 'InvalidTransformDataError(bytes transformData)', { + constructor(errorCode?: InvalidTransformDataErrorCode, transformData?: string) { + super('InvalidTransformDataError', 'InvalidTransformDataError(uint8 errorCode, bytes transformData)', { + errorCode, transformData, }); } From ecfbd6280f245c2649bb4e66e9e03fe3006306da Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 28 May 2020 11:55:25 -0400 Subject: [PATCH 18/19] `@0x/contracts-zero-ex`: Address review feedback. --- .../errors/LibTransformERC20RichErrors.sol | 9 +- .../src/transformers/FillQuoteTransformer.sol | 125 ++++++++++-------- .../src/transformers/PayTakerTransformer.sol | 1 - .../src/transformers/WethTransformer.sol | 8 +- .../contracts/test/TestDelegateCaller.sol | 1 - .../zero-ex/src/transformer_data_encoders.ts | 16 ++- .../fill_quote_transformer_test.ts | 49 ++++--- .../transformers/weth_transformer_test.ts | 7 +- 8 files changed, 133 insertions(+), 83 deletions(-) diff --git a/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol index cc1352eac4..2cee9e18b7 100644 --- a/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol +++ b/contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol @@ -130,7 +130,13 @@ library LibTransformERC20RichErrors { ); } + enum InvalidTransformDataErrorCode { + INVALID_TOKENS, + INVALID_ARRAY_LENGTH + } + function InvalidTransformDataError( + InvalidTransformDataErrorCode errorCode, bytes memory transformData ) internal @@ -138,7 +144,8 @@ library LibTransformERC20RichErrors { returns (bytes memory) { return abi.encodeWithSelector( - bytes4(keccak256("InvalidTransformDataError(bytes)")), + bytes4(keccak256("InvalidTransformDataError(uint8,bytes)")), + errorCode, transformData ); } diff --git a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol index 39604ce763..bd76a7c4d1 100644 --- a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol @@ -35,8 +35,16 @@ import "./LibERC20Transformer.sol"; contract FillQuoteTransformer is Transformer { + /// @dev Whether we are performing a market sell or buy. + enum Side { + Sell, + Buy + } + /// @dev Transform data to ABI-encode and pass into `transform()`. struct TransformData { + // Whether we aer performing a market sell or buy. + Side side; // The token being sold. // This should be an actual token, not the ETH pseudo-token. IERC20TokenV06 sellToken; @@ -52,11 +60,10 @@ contract FillQuoteTransformer is // For sells, this will be the maximum sell amount (taker asset). // For buys, this will be the maximum buy amount (maker asset). uint256[] maxOrderFillAmounts; - // Amount of `sellToken` to sell. May be `uint256(-1)` to sell entire - // amount of `sellToken` received. Zero if performing a market buy. - uint256 sellAmount; - // Amount of `buyToken` to buy. Zero if performing a market sell. - uint256 buyAmount; + // Amount of `sellToken` to sell or `buyToken` to buy. + // For sells, this may be `uint256(-1)` to sell the entire balance of + // `sellToken`. + uint256 fillAmount; } /// @dev Results of a call to `_fillOrder()`. @@ -96,7 +103,6 @@ contract FillQuoteTransformer is /// @dev Sell this contract's entire balance of of `sellToken` in exchange /// for `buyToken` by filling `orders`. Protocol fees should be attached /// to this call. `buyToken` and excess ETH will be transferred back to the caller. - /// This function cannot be re-entered. /// @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 @@ -113,23 +119,29 @@ contract FillQuoteTransformer is TransformData memory data = abi.decode(data_, (TransformData)); // Validate data fields. - if (data.sellToken.isTokenETH() || - data.buyToken.isTokenETH() || - data.orders.length != data.signatures.length) - { - LibTransformERC20RichErrors.InvalidTransformDataError(data_).rrevert(); + if (data.sellToken.isTokenETH() || data.buyToken.isTokenETH()) { + LibTransformERC20RichErrors.InvalidTransformDataError( + LibTransformERC20RichErrors.InvalidTransformDataErrorCode.INVALID_TOKENS, + data_ + ).rrevert(); + } + if (data.orders.length != data.signatures.length) { + LibTransformERC20RichErrors.InvalidTransformDataError( + LibTransformERC20RichErrors.InvalidTransformDataErrorCode.INVALID_ARRAY_LENGTH, + data_ + ).rrevert(); } - // If `sellAmount == -1` and `buyAmount == 0` then we are selling - // the entire balance of `sellToken`. This is useful in cases where - // the exact sell amount is not exactly known in advance, like when - // unwrapping Chai/cUSDC/cDAI. - if (data.sellAmount == uint256(-1) && data.buyAmount == 0) { - data.sellAmount = data.sellToken.getTokenBalanceOf(address(this)); + if (data.side == Side.Sell && data.fillAmount == uint256(-1)) { + // If `sellAmount == -1 then we are selling + // the entire balance of `sellToken`. This is useful in cases where + // the exact sell amount is not exactly known in advance, like when + // unwrapping Chai/cUSDC/cDAI. + data.fillAmount = data.sellToken.getTokenBalanceOf(address(this)); } // Approve the ERC20 proxy to spend `sellToken`. - data.sellToken.approveIfBelow(erc20Proxy, data.sellAmount); + data.sellToken.approveIfBelow(erc20Proxy, data.fillAmount); // Fill the orders. uint256 singleProtocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice); @@ -138,14 +150,14 @@ contract FillQuoteTransformer is uint256 soldAmount = 0; for (uint256 i = 0; i < data.orders.length; ++i) { // Check if we've hit our targets. - if (data.buyAmount == 0) { + if (data.side == Side.Sell) { // Market sell check. - if (soldAmount >= data.sellAmount) { + if (soldAmount >= data.fillAmount) { break; } } else { // Market buy check. - if (boughtAmount >= data.buyAmount) { + if (boughtAmount >= data.fillAmount) { break; } } @@ -159,14 +171,14 @@ contract FillQuoteTransformer is // Fill the order. FillOrderResults memory results; - if (data.buyAmount == 0) { + if (data.side == Side.Sell) { // Market sell. results = _sellToOrder( data.buyToken, data.sellToken, data.orders[i], data.signatures[i], - data.sellAmount.safeSub(soldAmount).min256( + data.fillAmount.safeSub(soldAmount).min256( data.maxOrderFillAmounts.length > i ? data.maxOrderFillAmounts[i] : uint256(-1) @@ -180,7 +192,7 @@ contract FillQuoteTransformer is data.sellToken, data.orders[i], data.signatures[i], - data.buyAmount.safeSub(boughtAmount).min256( + data.fillAmount.safeSub(boughtAmount).min256( data.maxOrderFillAmounts.length > i ? data.maxOrderFillAmounts[i] : uint256(-1) @@ -196,24 +208,24 @@ contract FillQuoteTransformer is } // Ensure we hit our targets. - if (data.buyAmount == 0) { + if (data.side == Side.Sell) { // Market sell check. - if (soldAmount < data.sellAmount) { + if (soldAmount < data.fillAmount) { LibTransformERC20RichErrors .IncompleteFillSellQuoteError( address(data.sellToken), soldAmount, - data.sellAmount + data.fillAmount ).rrevert(); } } else { // Market buy check. - if (boughtAmount < data.buyAmount) { + if (boughtAmount < data.fillAmount) { LibTransformERC20RichErrors .IncompleteFillBuyQuoteError( address(data.buyToken), boughtAmount, - data.buyAmount + data.fillAmount ).rrevert(); } } @@ -238,9 +250,8 @@ contract FillQuoteTransformer is private returns (FillOrderResults memory results) { - IERC20TokenV06 takerFeeToken = order.takerFeeAssetData.length == 0 - ? IERC20TokenV06(address(0)) - : _getTokenFromERC20AssetData(order.takerFeeAssetData); + IERC20TokenV06 takerFeeToken = + _getTokenFromERC20AssetData(order.takerFeeAssetData); uint256 takerTokenFillAmount = sellAmount; @@ -261,7 +272,7 @@ contract FillQuoteTransformer is takerTokenFillAmount = LibMathV06.getPartialAmountCeil( order.takerAssetAmount, order.takerAssetAmount.safeAdd(order.takerFee), - takerTokenFillAmount + sellAmount ); } else { // Only support taker or maker asset denominated taker fees. @@ -306,24 +317,27 @@ contract FillQuoteTransformer is private returns (FillOrderResults memory results) { - IERC20TokenV06 takerFeeToken = order.takerFeeAssetData.length == 0 - ? IERC20TokenV06(address(0)) - : _getTokenFromERC20AssetData(order.takerFeeAssetData); - - uint256 makerTokenFillAmount = buyAmount; + IERC20TokenV06 takerFeeToken = + _getTokenFromERC20AssetData(order.takerFeeAssetData); + // Compute the default taker token fill amount. + uint256 takerTokenFillAmount = LibMathV06.getPartialAmountCeil( + buyAmount, + order.makerAssetAmount, + order.takerAssetAmount + ); if (order.takerFee != 0) { if (takerFeeToken == makerToken) { // Taker fee is payable in the maker token. - // Increase the fill amount to account for maker tokens being - // lost to the taker fee. - // makerTokenFillAmount' = - // (order.makerAssetAmount * makerTokenFillAmount) / + // Adjust the taker token fill amount to account for maker + // tokens being lost to the taker fee. + // takerTokenFillAmount' = + // (order.takerAssetAmount * buyAmount) / // (order.makerAssetAmount - order.takerFee) - makerTokenFillAmount = LibMathV06.getPartialAmountCeil( - order.makerAssetAmount, + takerTokenFillAmount = LibMathV06.getPartialAmountCeil( + buyAmount, order.makerAssetAmount.safeSub(order.takerFee), - makerTokenFillAmount + order.takerAssetAmount ); // Approve the proxy to spend the maker token. // It isn't worth computing the actual taker fee @@ -338,14 +352,10 @@ contract FillQuoteTransformer is } } - // Convert maker fill amount to taker fill amount. - uint256 takerTokenFillAmount = LibSafeMathV06.min256( + // Clamp to order size. + takerTokenFillAmount = LibSafeMathV06.min256( order.takerAssetAmount, - LibMathV06.getPartialAmountCeil( - makerTokenFillAmount, - order.makerAssetAmount, - order.takerAssetAmount - ) + takerTokenFillAmount ); // Perform the fill. @@ -380,7 +390,7 @@ contract FillQuoteTransformer is returns (FillOrderResults memory results) { // Track changes in the maker token balance. - results.makerTokenBoughtAmount = makerToken.balanceOf(address(this)); + uint256 initialMakerTokenBalance = makerToken.balanceOf(address(this)); try exchange.fillOrder {value: protocolFee} @@ -389,7 +399,7 @@ contract FillQuoteTransformer is { // Update maker quantity based on changes in token balances. results.makerTokenBoughtAmount = makerToken.balanceOf(address(this)) - .safeSub(results.makerTokenBoughtAmount); + .safeSub(initialMakerTokenBalance); // We can trust the other fill result quantities. results.protocolFeePaid = fillResults.protocolFeePaid; results.takerTokenSoldAmount = fillResults.takerAssetFilledAmount; @@ -400,18 +410,21 @@ contract FillQuoteTransformer is results.takerTokenSoldAmount.safeAdd(fillResults.takerFeePaid); } } catch (bytes memory) { - // If the fill fails, zero out fill quantities. - results.makerTokenBoughtAmount = 0; + // Swallow failures, leaving all results as zero. } } /// @dev Extract the token from plain ERC20 asset data. + /// If the asset-data is empty, a zero token address will be returned. /// @param assetData The order asset data. function _getTokenFromERC20AssetData(bytes memory assetData) private pure returns (IERC20TokenV06 token) { + if (assetData.length == 0) { + return IERC20TokenV06(address(0)); + } if (assetData.length != 36 || LibBytesV06.readBytes4(assetData, 0) != ERC20_ASSET_PROXY_ID) { diff --git a/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol index 7c09c9ddd4..095aa08b4a 100644 --- a/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol @@ -49,7 +49,6 @@ contract PayTakerTransformer is /// @dev Create this contract. /// @param deploymentNonce_ The nonce of the deployer when deploying this contract. - /// @dev Construct the transformer and store the WETH address in an immutable. constructor(uint256 deploymentNonce_) public Transformer(deploymentNonce_) diff --git a/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol b/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol index 35678dbf17..78a442b2de 100644 --- a/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol @@ -47,10 +47,9 @@ contract WethTransformer is using LibSafeMathV06 for uint256; using LibERC20Transformer for IERC20TokenV06; - /// @dev Create this contract. + /// @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. - /// @dev Construct the transformer and store the WETH address in an immutable. constructor(IEtherTokenV06 weth_, uint256 deploymentNonce_) public Transformer(deploymentNonce_) @@ -74,7 +73,10 @@ contract WethTransformer is { TransformData memory data = abi.decode(data_, (TransformData)); if (!data.token.isTokenETH() && data.token != weth) { - LibTransformERC20RichErrors.InvalidTransformDataError(data_).rrevert(); + LibTransformERC20RichErrors.InvalidTransformDataError( + LibTransformERC20RichErrors.InvalidTransformDataErrorCode.INVALID_TOKENS, + data_ + ).rrevert(); } uint256 amount = data.amount; diff --git a/contracts/zero-ex/contracts/test/TestDelegateCaller.sol b/contracts/zero-ex/contracts/test/TestDelegateCaller.sol index 0fcbf3aef5..7996e03ecf 100644 --- a/contracts/zero-ex/contracts/test/TestDelegateCaller.sol +++ b/contracts/zero-ex/contracts/test/TestDelegateCaller.sol @@ -20,7 +20,6 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; - contract TestDelegateCaller { function executeDelegateCall( address target, diff --git a/contracts/zero-ex/src/transformer_data_encoders.ts b/contracts/zero-ex/src/transformer_data_encoders.ts index b44bea7631..d9da6a0e7c 100644 --- a/contracts/zero-ex/src/transformer_data_encoders.ts +++ b/contracts/zero-ex/src/transformer_data_encoders.ts @@ -26,6 +26,7 @@ export const fillQuoteTransformerDataEncoder = AbiEncoder.create([ name: 'data', type: 'tuple', components: [ + { name: 'side', type: 'uint8' }, { name: 'sellToken', type: 'address' }, { name: 'buyToken', type: 'address' }, { @@ -35,23 +36,30 @@ export const fillQuoteTransformerDataEncoder = AbiEncoder.create([ }, { name: 'signatures', type: 'bytes[]' }, { name: 'maxOrderFillAmounts', type: 'uint256[]' }, - { name: 'sellAmount', type: 'uint256' }, - { name: 'buyAmount', type: 'uint256' }, + { name: 'fillAmount', type: 'uint256' }, ], }, ]); +/** + * Market operation for `FillQuoteTransformerData`. + */ +export enum FillQuoteTransformerSide { + Sell, + Buy, +} + /** * `FillQuoteTransformer.TransformData` */ export interface FillQuoteTransformerData { + side: FillQuoteTransformerSide; sellToken: string; buyToken: string; orders: Array>; signatures: string[]; maxOrderFillAmounts: BigNumber[]; - sellAmount: BigNumber; - buyAmount: BigNumber; + fillAmount: BigNumber; } /** diff --git a/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts index 7f8c312e45..5460c62d94 100644 --- a/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts +++ b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts @@ -13,7 +13,11 @@ import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils'; import * as _ from 'lodash'; import { rlpEncodeNonce } from '../../src/nonce_utils'; -import { encodeFillQuoteTransformerData, FillQuoteTransformerData } from '../../src/transformer_data_encoders'; +import { + encodeFillQuoteTransformerData, + FillQuoteTransformerData, + FillQuoteTransformerSide, +} from '../../src/transformer_data_encoders'; import { artifacts } from '../artifacts'; import { FillQuoteTransformerContract, @@ -217,13 +221,13 @@ blockchainTests.resets('FillQuoteTransformer', env => { function encodeTransformData(fields: Partial = {}): string { return encodeFillQuoteTransformerData({ + side: FillQuoteTransformerSide.Sell, sellToken: takerToken.address, buyToken: makerToken.address, orders: [], signatures: [], maxOrderFillAmounts: [], - sellAmount: MAX_UINT256, - buyAmount: ZERO_AMOUNT, + fillAmount: MAX_UINT256, ...fields, }); } @@ -455,7 +459,7 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, - sellAmount: qfr.takerAssetSpent, + fillAmount: qfr.takerAssetSpent, }), ) .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); @@ -479,7 +483,7 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, - sellAmount: qfr.takerAssetSpent, + fillAmount: qfr.takerAssetSpent, }), ) .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); @@ -614,7 +618,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, - buyAmount: qfr.makerAssetBought, + side: FillQuoteTransformerSide.Buy, + fillAmount: qfr.makerAssetBought, }), ) .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); @@ -636,7 +641,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, - buyAmount: qfr.makerAssetBought, + side: FillQuoteTransformerSide.Buy, + fillAmount: qfr.makerAssetBought, }), ) .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); @@ -661,7 +667,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, - buyAmount: qfr.makerAssetBought, + side: FillQuoteTransformerSide.Buy, + fillAmount: qfr.makerAssetBought, }), ) .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); @@ -684,7 +691,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, - buyAmount: qfr.makerAssetBought, + side: FillQuoteTransformerSide.Buy, + fillAmount: qfr.makerAssetBought, }), ) .awaitTransactionSuccessAsync({ value: maxProtocolFees }); @@ -709,7 +717,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, - buyAmount: qfr.makerAssetBought, + side: FillQuoteTransformerSide.Buy, + fillAmount: qfr.makerAssetBought, }), ) .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); @@ -736,7 +745,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, - buyAmount: qfr.makerAssetBought, + side: FillQuoteTransformerSide.Buy, + fillAmount: qfr.makerAssetBought, }), ) .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); @@ -760,7 +770,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, - buyAmount: qfr.makerAssetBought.plus(1), + side: FillQuoteTransformerSide.Buy, + fillAmount: qfr.makerAssetBought.plus(1), }), ) .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); @@ -789,7 +800,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, - buyAmount: qfr.makerAssetBought, + side: FillQuoteTransformerSide.Buy, + fillAmount: qfr.makerAssetBought, }), ) .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); @@ -812,7 +824,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, - buyAmount: qfr.makerAssetBought, + side: FillQuoteTransformerSide.Buy, + fillAmount: qfr.makerAssetBought, }), ) .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); @@ -835,7 +848,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, - buyAmount: qfr.makerAssetBought, + side: FillQuoteTransformerSide.Buy, + fillAmount: qfr.makerAssetBought, }), ) .awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid }); @@ -855,7 +869,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, - buyAmount: qfr.makerAssetBought, + side: FillQuoteTransformerSide.Buy, + fillAmount: qfr.makerAssetBought, // Skip the first order. maxOrderFillAmounts: [ZERO_AMOUNT], }), @@ -879,6 +894,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { encodeTransformData({ orders, signatures, + side: FillQuoteTransformerSide.Buy, + fillAmount: qfr.makerAssetBought, }), ) .callAsync({ value: qfr.protocolFeePaid }); diff --git a/contracts/zero-ex/test/transformers/weth_transformer_test.ts b/contracts/zero-ex/test/transformers/weth_transformer_test.ts index e5c1cc8c26..6548e40872 100644 --- a/contracts/zero-ex/test/transformers/weth_transformer_test.ts +++ b/contracts/zero-ex/test/transformers/weth_transformer_test.ts @@ -61,7 +61,12 @@ blockchainTests.resets('WethTransformer', env => { const tx = host .executeTransform(amount, transformer.address, data) .awaitTransactionSuccessAsync({ value: amount }); - return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidTransformDataError(data)); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.TransformERC20.InvalidTransformDataError( + ZeroExRevertErrors.TransformERC20.InvalidTransformDataErrorCode.InvalidTokens, + data, + ), + ); }); it('can unwrap WETH', async () => { From 112f4fc4f0ee15d20ee1b6874612f56386d72702 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 2 Jun 2020 22:01:22 -0400 Subject: [PATCH 19/19] `@0x/contracts-zero-ex`: Address review comments. --- .../src/transformers/FillQuoteTransformer.sol | 20 ++++++++++--------- .../src/transformers/LibERC20Transformer.sol | 4 ++-- .../src/transformers/PayTakerTransformer.sol | 10 ++++++---- .../src/transformers/WethTransformer.sol | 12 ++++++----- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol index bd76a7c4d1..eb7af401f3 100644 --- a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol @@ -35,6 +35,11 @@ import "./LibERC20Transformer.sol"; contract FillQuoteTransformer is Transformer { + using LibERC20TokenV06 for IERC20TokenV06; + using LibERC20Transformer for IERC20TokenV06; + using LibSafeMathV06 for uint256; + using LibRichErrorsV06 for bytes; + /// @dev Whether we are performing a market sell or buy. enum Side { Sell, @@ -77,18 +82,15 @@ contract FillQuoteTransformer is } /// @dev The Exchange ERC20Proxy ID. - bytes4 constant private ERC20_ASSET_PROXY_ID = 0xf47261b0; + bytes4 private constant ERC20_ASSET_PROXY_ID = 0xf47261b0; + /// @dev Maximum uint256 value. + uint256 private constant MAX_UINT256 = uint256(-1); /// @dev The Exchange contract. IExchange public immutable exchange; /// @dev The ERC20Proxy address. address public immutable erc20Proxy; - using LibERC20TokenV06 for IERC20TokenV06; - using LibERC20Transformer for IERC20TokenV06; - using LibSafeMathV06 for uint256; - using LibRichErrorsV06 for bytes; - /// @dev Create this contract. /// @param exchange_ The Exchange V3 instance. /// @param deploymentNonce_ The nonce of the deployer when deploying this contract. @@ -132,7 +134,7 @@ contract FillQuoteTransformer is ).rrevert(); } - if (data.side == Side.Sell && data.fillAmount == uint256(-1)) { + if (data.side == Side.Sell && data.fillAmount == MAX_UINT256) { // If `sellAmount == -1 then we are selling // the entire balance of `sellToken`. This is useful in cases where // the exact sell amount is not exactly known in advance, like when @@ -181,7 +183,7 @@ contract FillQuoteTransformer is data.fillAmount.safeSub(soldAmount).min256( data.maxOrderFillAmounts.length > i ? data.maxOrderFillAmounts[i] - : uint256(-1) + : MAX_UINT256 ), singleProtocolFee ); @@ -195,7 +197,7 @@ contract FillQuoteTransformer is data.fillAmount.safeSub(boughtAmount).min256( data.maxOrderFillAmounts.length > i ? data.maxOrderFillAmounts[i] - : uint256(-1) + : MAX_UINT256 ), singleProtocolFee ); diff --git a/contracts/zero-ex/contracts/src/transformers/LibERC20Transformer.sol b/contracts/zero-ex/contracts/src/transformers/LibERC20Transformer.sol index b0c08a0351..fe768b34ad 100644 --- a/contracts/zero-ex/contracts/src/transformers/LibERC20Transformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/LibERC20Transformer.sol @@ -25,11 +25,11 @@ import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; library LibERC20Transformer { + using LibERC20TokenV06 for IERC20TokenV06; + /// @dev ETH pseudo-token address. address constant internal ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - using LibERC20TokenV06 for IERC20TokenV06; - /// @dev Transfer ERC20 tokens and ETH. /// @param token An ERC20 or the ETH pseudo-token address (`ETH_TOKEN_ADDRESS`). /// @param to The recipient. diff --git a/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol index 095aa08b4a..53de743e77 100644 --- a/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol @@ -33,6 +33,9 @@ contract PayTakerTransformer is Transformer { // solhint-disable no-empty-blocks + using LibRichErrorsV06 for bytes; + using LibSafeMathV06 for uint256; + using LibERC20Transformer for IERC20TokenV06; /// @dev Transform data to ABI-encode and pass into `transform()`. struct TransformData { @@ -43,9 +46,8 @@ contract PayTakerTransformer is uint256[] amounts; } - using LibRichErrorsV06 for bytes; - using LibSafeMathV06 for uint256; - using LibERC20Transformer for IERC20TokenV06; + /// @dev Maximum uint256 value. + uint256 private constant MAX_UINT256 = uint256(-1); /// @dev Create this contract. /// @param deploymentNonce_ The nonce of the deployer when deploying this contract. @@ -76,7 +78,7 @@ contract PayTakerTransformer is // The `amounts` array can be shorter than the `tokens` array. // Missing elements are treated as `uint256(-1)`. uint256 amount = data.amounts.length > i ? data.amounts[i] : uint256(-1); - if (amount == uint256(-1)) { + if (amount == MAX_UINT256) { amount = data.tokens[i].getTokenBalanceOf(address(this)); } if (amount != 0) { diff --git a/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol b/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol index 78a442b2de..bba2d6440e 100644 --- a/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/WethTransformer.sol @@ -31,6 +31,10 @@ import "./LibERC20Transformer.sol"; contract WethTransformer is Transformer { + using LibRichErrorsV06 for bytes; + using LibSafeMathV06 for uint256; + using LibERC20Transformer for IERC20TokenV06; + /// @dev Transform data to ABI-encode and pass into `transform()`. struct TransformData { // The token to wrap/unwrap. Must be either ETH or WETH. @@ -42,10 +46,8 @@ contract WethTransformer is /// @dev The WETH contract address. IEtherTokenV06 public immutable weth; - - using LibRichErrorsV06 for bytes; - using LibSafeMathV06 for uint256; - using LibERC20Transformer for IERC20TokenV06; + /// @dev Maximum uint256 value. + uint256 private constant MAX_UINT256 = uint256(-1); /// @dev Construct the transformer and store the WETH address in an immutable. /// @param weth_ The weth token. @@ -80,7 +82,7 @@ contract WethTransformer is } uint256 amount = data.amount; - if (amount == uint256(-1)) { + if (amount == MAX_UINT256) { amount = data.token.getTokenBalanceOf(address(this)); }