From 1cdd82178ff630827095e778a222fafa4161969e Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Tue, 4 Dec 2018 15:23:25 -0800 Subject: ComplianceForwarder renamed to BalanceThresholdFilter --- contracts/test-utils/src/types.ts | 2 +- .../BalanceThresholdFilter.sol | 323 ++++++++++++++++++ .../interfaces/IThresholdAsset.sol | 30 ++ .../CompliantForwarder/CompliantForwarder.sol | 323 ------------------ .../test/extensions/balance_threshold_filter.ts | 366 +++++++++++++++++++++ .../test/extensions/compliant_forwarder.ts | 366 --------------------- 6 files changed, 720 insertions(+), 690 deletions(-) create mode 100644 packages/contracts/contracts/extensions/BalanceThresholdFilter/BalanceThresholdFilter.sol create mode 100644 packages/contracts/contracts/extensions/BalanceThresholdFilter/interfaces/IThresholdAsset.sol delete mode 100644 packages/contracts/contracts/extensions/CompliantForwarder/CompliantForwarder.sol create mode 100644 packages/contracts/test/extensions/balance_threshold_filter.ts delete mode 100644 packages/contracts/test/extensions/compliant_forwarder.ts diff --git a/contracts/test-utils/src/types.ts b/contracts/test-utils/src/types.ts index 46b7ad941..cbdd513eb 100644 --- a/contracts/test-utils/src/types.ts +++ b/contracts/test-utils/src/types.ts @@ -105,7 +105,7 @@ export enum ContractName { Authorizable = 'Authorizable', Whitelist = 'Whitelist', Forwarder = 'Forwarder', - CompliantForwarder = 'CompliantForwarder', + BalanceThresholdFilter = 'BalanceThresholdFilter', } export interface SignedTransaction { diff --git a/packages/contracts/contracts/extensions/BalanceThresholdFilter/BalanceThresholdFilter.sol b/packages/contracts/contracts/extensions/BalanceThresholdFilter/BalanceThresholdFilter.sol new file mode 100644 index 000000000..93b63eb52 --- /dev/null +++ b/packages/contracts/contracts/extensions/BalanceThresholdFilter/BalanceThresholdFilter.sol @@ -0,0 +1,323 @@ +/* + + Copyright 2018 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.4.24; +pragma experimental ABIEncoderV2; + +import "../../protocol/Exchange/interfaces/IExchange.sol"; +import "../../utils/LibBytes/LibBytes.sol"; +import "../../utils/ExchangeSelectors/ExchangeSelectors.sol"; +import "./interfaces/IThresholdAsset.sol"; + +contract BalanceThresholdFilter is ExchangeSelectors { + + using LibBytes for bytes; + + IExchange internal EXCHANGE; + IThresholdAsset internal THRESHOLD_ASSET; + + event ValidatedAddresses ( + address[] addresses + ); + + constructor(address exchange, address thresholdAsset) + public + { + EXCHANGE = IExchange(exchange); + THRESHOLD_ASSET = IThresholdAsset(thresholdAsset); + } + + function executeTransaction( + uint256 salt, + address signerAddress, + bytes signedExchangeTransaction, + bytes signature + ) + external + { + // Addresses that are validated below. + address[] memory validatedAddresses; + + /** + * Do not add variables after this point. + * The assembly block may overwrite their values. + */ + + // Validate addresses + assembly { + /** + * Emulates the `calldataload` opcode on the embedded Exchange calldata, + * which is accessed through `signedExchangeTransaction`. + * @param offset - Offset into the Exchange calldata. + * @return value - Corresponding 32 byte value stored at `offset`. + */ + function exchangeCalldataload(offset) -> value { + // Pointer to exchange transaction + // 0x04 for calldata selector + // 0x40 to access `signedExchangeTransaction`, which is the third parameter + let exchangeTxPtr := calldataload(0x44) + + // Offset into Exchange calldata + // We compute this by adding 0x24 to the `exchangeTxPtr` computed above. + // 0x04 for calldata selector + // 0x20 for length field of `signedExchangeTransaction` + let exchangeCalldataOffset := add(exchangeTxPtr, add(0x24, offset)) + value := calldataload(exchangeCalldataOffset) + } + + /** + * Convenience function that skips the 4 byte selector when loading + * from the embedded Exchange calldata. + * @param offset - Offset into the Exchange calldata (minus the 4 byte selector) + * @return value - Corresponding 32 byte value stored at `offset` + 4. + */ + function loadExchangeData(offset) -> value { + value := exchangeCalldataload(add(offset, 0x4)) + } + + /** + * A running list is maintained of addresses to validate. + * This function records an address in this array. + * @param addressToValidate - Address to record for validation. + * @note - Variables are scoped but names are not, so we append + * underscores to names that share the global namespace. + */ + function recordAddressToValidate(addressToValidate) { + // Compute `addressesToValidate` memory offset + let addressesToValidate_ := mload(0x40) + let nAddressesToValidate_ := mload(addressesToValidate_) + + // Increment length + nAddressesToValidate_ := add(mload(addressesToValidate_), 0x01) + mstore(addressesToValidate_, nAddressesToValidate_) + + // Append address to validate + let offset := mul(nAddressesToValidate_, 0x20) + mstore(add(addressesToValidate_, offset), addressToValidate) + } + + /** + * Extracts the maker address from an order stored in the Exchange calldata + * (which is embedded in `signedExchangeTransaction`), and records it in + * the running list of addresses to validate. + * @param orderParamIndex - Index of the order in the Exchange function's signature + */ + function recordMakerAddressFromOrder(orderParamIndex) { + let orderPtr := loadExchangeData(orderParamIndex) + let makerAddress := loadExchangeData(orderPtr) + recordAddressToValidate(makerAddress) + } + + /** + * Extracts the maker addresses from an array of orders stored in the Exchange calldata + * (which is embedded in `signedExchangeTransaction`), and records them in + * the running list of addresses to validate. + * @param orderArrayParamIndex - Index of the order array in the Exchange function's signature + */ + function recordMakerAddressesFromOrderArray(orderArrayParamIndex) { + let orderArrayPtr := loadExchangeData(0x0) + let orderArrayLength := loadExchangeData(orderArrayPtr) + let orderArrayElementPtr := add(orderArrayPtr, 0x20) + let orderArrayElementEndPtr := add(orderArrayElementPtr, mul(orderArrayLength, 0x20)) + for {let orderPtrOffset := orderArrayElementPtr} lt(orderPtrOffset, orderArrayElementEndPtr) {orderPtrOffset := add(orderPtrOffset, 0x20)} { + let orderPtr := loadExchangeData(orderPtrOffset) + let makerAddress := loadExchangeData(add(orderPtr, orderArrayElementPtr)) + recordAddressToValidate(makerAddress) + } + } + + /** + * Records address of signer in the running list of addresses to validate. + * @note: We cannot access `signerAddress` directly from within the asm function, + * so it is loaded from the calldata. + */ + function recordSignerAddress() { + // Load the signer address from calldata + // 0x04 for selector + // 0x20 to access `signerAddress`, which is the second parameter. + let signerAddress_ := calldataload(0x24) + recordAddressToValidate(signerAddress_) + } + + /** + * Records addresses to be validated when Exchange transaction is a batch fill variant. + * This is one of: batchFillOrders, batchFillOrKillOrders, batchFillNoThrow + * Reference signature: (Order[],uint256[],bytes[]) + */ + function recordAddressesForBatchFillVariant() { + // Record maker addresses from order array (parameter index 0) + // The signer is the taker for these orders and must also be validated. + recordMakerAddressesFromOrderArray(0) + recordSignerAddress() + } + + /** + * Records addresses to be validated when Exchange transaction is a fill order variant. + * This is one of: fillOrder, fillOrKillOrder, fillOrderNoThrow + * Reference signature: (Order,uint256,bytes) + */ + function recordAddressesForFillOrderVariant() { + // Record maker address from the order (param index 0) + // The signer is the taker for this order and must also be validated. + recordMakerAddressFromOrder(0) + recordSignerAddress() + } + + /** + * Records addresses to be validated when Exchange transaction is a market fill variant. + * This is one of: marketBuyOrders, marketBuyOrdersNoThrow, marketSellOrders, marketSellOrdersNoThrow + * Reference signature: (Order[],uint256,bytes[]) + */ + function recordAddressesForMarketFillVariant() { + // Record maker addresses from order array (parameter index 0) + // The signer is the taker for these orders and must also be validated. + recordMakerAddressesFromOrderArray(0) + recordSignerAddress() + } + + /** + * Records addresses to be validated when Exchange transaction is matchOrders. + * Reference signature: matchOrders(Order,Order) + */ + function recordAddressesForMatchOrders() { + // Record maker address from both orders (param indices 0 & 1). + // The signer is the taker and must also be validated. + recordMakerAddressFromOrder(0) + recordMakerAddressFromOrder(1) + recordSignerAddress() + } + + ///// Record Addresses to Validate ///// + + // Addresses needing validation depends on which Exchange function is being called. + // Step 1/2 Read the exchange function selector. + let exchangeFunctionSelector := and( + exchangeCalldataload(0x0), + 0xffffffff00000000000000000000000000000000000000000000000000000000 + ) + + // Step 2/2 Extract addresses to validate based on this selector. + // See ../../utils/ExchangeSelectors/ExchangeSelectors.sol for selectors + switch exchangeFunctionSelector + case 0x297bb70b00000000000000000000000000000000000000000000000000000000 { recordAddressesForBatchFillVariant() } // batchFillOrders + case 0x50dde19000000000000000000000000000000000000000000000000000000000 { recordAddressesForBatchFillVariant() } // batchFillOrdersNoThrow + case 0x4d0ae54600000000000000000000000000000000000000000000000000000000 { recordAddressesForBatchFillVariant() } // batchFillOrKillOrders + case 0xb4be83d500000000000000000000000000000000000000000000000000000000 { recordAddressesForFillOrderVariant() } // fillOrder + case 0x3e228bae00000000000000000000000000000000000000000000000000000000 { recordAddressesForFillOrderVariant() } // fillOrderNoThrow + case 0x64a3bc1500000000000000000000000000000000000000000000000000000000 { recordAddressesForFillOrderVariant() } // fillOrKillOrder + case 0xe5fa431b00000000000000000000000000000000000000000000000000000000 { recordAddressesForMarketFillVariant() } // marketBuyOrders + case 0xa3e2038000000000000000000000000000000000000000000000000000000000 { recordAddressesForMarketFillVariant() } // marketBuyOrdersNoThrow + case 0x7e1d980800000000000000000000000000000000000000000000000000000000 { recordAddressesForMarketFillVariant() } // marketSellOrders + case 0xdd1c7d1800000000000000000000000000000000000000000000000000000000 { recordAddressesForMarketFillVariant() } // marketSellOrdersNoThrow + case 0x3c28d86100000000000000000000000000000000000000000000000000000000 { recordAddressesForMatchOrders() } // matchOrders + case 0xd46b02c300000000000000000000000000000000000000000000000000000000 {} // cancelOrder + case 0x4ac1478200000000000000000000000000000000000000000000000000000000 {} // batchCancelOrders + case 0x4f9559b100000000000000000000000000000000000000000000000000000000 {} // cancelOrdersUpTo + default { + // Revert with `Error("INVALID_OR_BLOCKED_EXCHANGE_SELECTOR")` + mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(0x20, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(0x40, 0x00000024494e56414c49445f4f525f424c4f434b45445f45584348414e47455f) + mstore(0x60, 0x53454c4543544f52000000000000000000000000000000000000000000000000) + mstore(0x80, 0x00000000) + // Revert length calculation: + // 4 -- error selector + // 32 -- offset to string + // 32 -- string length field + // 64 -- strlen(INVALID_OR_BLOCKED_EXCHANGE_SELECTOR) rounded up to nearest 32-byte word. + revert(0, 132) + } + + ///// Validate Recorded Addresses ///// + + // Load from memory the addresses to validate + let addressesToValidate := mload(0x40) + let addressesToValidateLength := mload(addressesToValidate) + let addressesToValidateElementPtr := add(addressesToValidate, 0x20) + let addressesToValidateElementEndPtr := add(addressesToValidateElementPtr, mul(addressesToValidateLength, 0x20)) + + // Set free memory pointer to after `addressesToValidate` array. + // This is to avoid corruption when making calls in the loop below. + let freeMemPtr := addressesToValidateElementEndPtr + mstore(0x40, freeMemPtr) + + // Validate addresses + let thresholdAssetAddress := sload(THRESHOLD_ASSET_slot) + for {let addressToValidate := addressesToValidateElementPtr} lt(addressToValidate, addressesToValidateElementEndPtr) {addressToValidate := add(addressToValidate, 0x20)} { + // Construct calldata for `THRESHOLD_ASSET.balanceOf` + mstore(freeMemPtr, 0x70a0823100000000000000000000000000000000000000000000000000000000) + mstore(add(4, freeMemPtr), mload(addressToValidate)) + + // call `THRESHOLD_ASSET.balanceOf` + let success := call( + gas, // forward all gas + thresholdAssetAddress, // call address of asset proxy + 0, // don't send any ETH + freeMemPtr, // pointer to start of input + 0x24, // length of input (one padded address) + freeMemPtr, // write output to next free memory offset + 0x20 // reserve space for return balance (0x20 bytes) + ) + if eq(success, 0) { + // @TODO Revert with `Error("BALANCE_QUERY_FAILED")` + mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(0x20, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(0x40, 0x0000001442414c414e43455f51554552595f4641494c45440000000000000000) + mstore(0x60, 0x00000000) + // Revert length calculation: + // 4 -- error selector + // 32 -- offset to string + // 32 -- string length field + // 32 -- strlen(BALANCE_QUERY_FAILED) rounded up to nearest 32-byte word. + revert(0, 100) + } + + // Revert if balance not held + let addressBalance := mload(freeMemPtr) + if eq(addressBalance, 0) { + // Revert with `Error("AT_LEAST_ONE_ADDRESS_DOES_NOT_MEET_BALANCE_THRESHOLD")` + mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(0x20, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(0x40, 0x0000003441545f4c454153545f4f4e455f414444524553535f444f45535f4e4f) + mstore(0x60, 0x545f4d4545545f42414c414e43455f5448524553484f4c440000000000000000) + mstore(0x80, 0x00000000) + // Revert length calculation: + // 4 -- error selector + // 32 -- offset to string + // 32 -- string length field + // 64 -- strlen(AT_LEAST_ONE_ADDRESS_DOES_NOT_MEET_BALANCE_THRESHOLD) rounded up to nearest 32-byte word. + revert(0, 132) + } + } + + // Record validated addresses + validatedAddresses := addressesToValidate + } + + ///// If we hit this point then all addresses are valid ///// + emit ValidatedAddresses(validatedAddresses); + + // All addresses are valid. Execute fillOrder. + EXCHANGE.executeTransaction( + salt, + signerAddress, + signedExchangeTransaction, + signature + ); + } +} \ No newline at end of file diff --git a/packages/contracts/contracts/extensions/BalanceThresholdFilter/interfaces/IThresholdAsset.sol b/packages/contracts/contracts/extensions/BalanceThresholdFilter/interfaces/IThresholdAsset.sol new file mode 100644 index 000000000..61acaba0a --- /dev/null +++ b/packages/contracts/contracts/extensions/BalanceThresholdFilter/interfaces/IThresholdAsset.sol @@ -0,0 +1,30 @@ +/* + + Copyright 2018 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.4.24; + +contract IThresholdAsset { + + /// @param _owner The address from which the balance will be retrieved + /// @return Balance of owner + function balanceOf(address _owner) + external + view + returns (uint256); + +} \ No newline at end of file diff --git a/packages/contracts/contracts/extensions/CompliantForwarder/CompliantForwarder.sol b/packages/contracts/contracts/extensions/CompliantForwarder/CompliantForwarder.sol deleted file mode 100644 index d33f4f398..000000000 --- a/packages/contracts/contracts/extensions/CompliantForwarder/CompliantForwarder.sol +++ /dev/null @@ -1,323 +0,0 @@ -/* - - Copyright 2018 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.4.24; -pragma experimental ABIEncoderV2; - -import "../../protocol/Exchange/interfaces/IExchange.sol"; -import "../../tokens/ERC721Token/IERC721Token.sol"; -import "../../utils/LibBytes/LibBytes.sol"; -import "../../utils/ExchangeSelectors/ExchangeSelectors.sol"; - -contract CompliantForwarder is ExchangeSelectors{ - - using LibBytes for bytes; - - IExchange internal EXCHANGE; - IERC721Token internal COMPLIANCE_TOKEN; - - event ValidatedAddresses ( - address[] addresses - ); - - constructor(address exchange, address complianceToken) - public - { - EXCHANGE = IExchange(exchange); - COMPLIANCE_TOKEN = IERC721Token(complianceToken); - } - - function executeTransaction( - uint256 salt, - address signerAddress, - bytes signedExchangeTransaction, - bytes signature - ) - external - { - // Addresses that are validated below. - address[] memory validatedAddresses; - - /** - * Do not add variables after this point. - * The assembly block may overwrite their values. - */ - - // Validate addresses - assembly { - /** - * Emulates the `calldataload` opcode on the embedded Exchange calldata, - * which is accessed through `signedExchangeTransaction`. - * @param offset - Offset into the Exchange calldata. - * @return value - Corresponding 32 byte value stored at `offset`. - */ - function exchangeCalldataload(offset) -> value { - // Pointer to exchange transaction - // 0x04 for calldata selector - // 0x40 to access `signedExchangeTransaction`, which is the third parameter - let exchangeTxPtr := calldataload(0x44) - - // Offset into Exchange calldata - // We compute this by adding 0x24 to the `exchangeTxPtr` computed above. - // 0x04 for calldata selector - // 0x20 for length field of `signedExchangeTransaction` - let exchangeCalldataOffset := add(exchangeTxPtr, add(0x24, offset)) - value := calldataload(exchangeCalldataOffset) - } - - /** - * Convenience function that skips the 4 byte selector when loading - * from the embedded Exchange calldata. - * @param offset - Offset into the Exchange calldata (minus the 4 byte selector) - * @return value - Corresponding 32 byte value stored at `offset` + 4. - */ - function loadExchangeData(offset) -> value { - value := exchangeCalldataload(add(offset, 0x4)) - } - - /** - * A running list is maintained of addresses to validate. - * This function records an address in this array. - * @param addressToValidate - Address to record for validation. - * @note - Variables are scoped but names are not, so we append - * underscores to names that share the global namespace. - */ - function recordAddressToValidate(addressToValidate) { - // Compute `addressesToValidate` memory offset - let addressesToValidate_ := mload(0x40) - let nAddressesToValidate_ := mload(addressesToValidate_) - - // Increment length - nAddressesToValidate_ := add(mload(addressesToValidate_), 0x01) - mstore(addressesToValidate_, nAddressesToValidate_) - - // Append address to validate - let offset := mul(nAddressesToValidate_, 0x20) - mstore(add(addressesToValidate_, offset), addressToValidate) - } - - /** - * Extracts the maker address from an order stored in the Exchange calldata - * (which is embedded in `signedExchangeTransaction`), and records it in - * the running list of addresses to validate. - * @param orderParamIndex - Index of the order in the Exchange function's signature - */ - function recordMakerAddressFromOrder(orderParamIndex) { - let orderPtr := loadExchangeData(orderParamIndex) - let makerAddress := loadExchangeData(orderPtr) - recordAddressToValidate(makerAddress) - } - - /** - * Extracts the maker addresses from an array of orders stored in the Exchange calldata - * (which is embedded in `signedExchangeTransaction`), and records them in - * the running list of addresses to validate. - * @param orderArrayParamIndex - Index of the order array in the Exchange function's signature - */ - function recordMakerAddressesFromOrderArray(orderArrayParamIndex) { - let orderArrayPtr := loadExchangeData(0x0) - let orderArrayLength := loadExchangeData(orderArrayPtr) - let orderArrayElementPtr := add(orderArrayPtr, 0x20) - let orderArrayElementEndPtr := add(orderArrayElementPtr, mul(orderArrayLength, 0x20)) - for {let orderPtrOffset := orderArrayElementPtr} lt(orderPtrOffset, orderArrayElementEndPtr) {orderPtrOffset := add(orderPtrOffset, 0x20)} { - let orderPtr := loadExchangeData(orderPtrOffset) - let makerAddress := loadExchangeData(add(orderPtr, orderArrayElementPtr)) - recordAddressToValidate(makerAddress) - } - } - - /** - * Records address of signer in the running list of addresses to validate. - * @note: We cannot access `signerAddress` directly from within the asm function, - * so it is loaded from the calldata. - */ - function recordSignerAddress() { - // Load the signer address from calldata - // 0x04 for selector - // 0x20 to access `signerAddress`, which is the second parameter. - let signerAddress_ := calldataload(0x24) - recordAddressToValidate(signerAddress_) - } - - /** - * Records addresses to be validated when Exchange transaction is a batch fill variant. - * This is one of: batchFillOrders, batchFillOrKillOrders, batchFillNoThrow - * Reference signature: (Order[],uint256[],bytes[]) - */ - function recordAddressesForBatchFillVariant() { - // Record maker addresses from order array (parameter index 0) - // The signer is the taker for these orders and must also be validated. - recordMakerAddressesFromOrderArray(0) - recordSignerAddress() - } - - /** - * Records addresses to be validated when Exchange transaction is a fill order variant. - * This is one of: fillOrder, fillOrKillOrder, fillOrderNoThrow - * Reference signature: (Order,uint256,bytes) - */ - function recordAddressesForFillOrderVariant() { - // Record maker address from the order (param index 0) - // The signer is the taker for this order and must also be validated. - recordMakerAddressFromOrder(0) - recordSignerAddress() - } - - /** - * Records addresses to be validated when Exchange transaction is a market fill variant. - * This is one of: marketBuyOrders, marketBuyOrdersNoThrow, marketSellOrders, marketSellOrdersNoThrow - * Reference signature: (Order[],uint256,bytes[]) - */ - function recordAddressesForMarketFillVariant() { - // Record maker addresses from order array (parameter index 0) - // The signer is the taker for these orders and must also be validated. - recordMakerAddressesFromOrderArray(0) - recordSignerAddress() - } - - /** - * Records addresses to be validated when Exchange transaction is matchOrders. - * Reference signature: matchOrders(Order,Order) - */ - function recordAddressesForMatchOrders() { - // Record maker address from both orders (param indices 0 & 1). - // The signer is the taker and must also be validated. - recordMakerAddressFromOrder(0) - recordMakerAddressFromOrder(1) - recordSignerAddress() - } - - ///// Record Addresses to Validate ///// - - // Addresses needing validation depends on which Exchange function is being called. - // Step 1/2 Read the exchange function selector. - let exchangeFunctionSelector := and( - exchangeCalldataload(0x0), - 0xffffffff00000000000000000000000000000000000000000000000000000000 - ) - - // Step 2/2 Extract addresses to validate based on this selector. - // See ../../utils/ExchangeSelectors/ExchangeSelectors.sol for selectors - switch exchangeFunctionSelector - case 0x297bb70b00000000000000000000000000000000000000000000000000000000 { recordAddressesForBatchFillVariant() } // batchFillOrders - case 0x50dde19000000000000000000000000000000000000000000000000000000000 { recordAddressesForBatchFillVariant() } // batchFillOrdersNoThrow - case 0x4d0ae54600000000000000000000000000000000000000000000000000000000 { recordAddressesForBatchFillVariant() } // batchFillOrKillOrders - case 0xb4be83d500000000000000000000000000000000000000000000000000000000 { recordAddressesForFillOrderVariant() } // fillOrder - case 0x3e228bae00000000000000000000000000000000000000000000000000000000 { recordAddressesForFillOrderVariant() } // fillOrderNoThrow - case 0x64a3bc1500000000000000000000000000000000000000000000000000000000 { recordAddressesForFillOrderVariant() } // fillOrKillOrder - case 0xe5fa431b00000000000000000000000000000000000000000000000000000000 { recordAddressesForMarketFillVariant() } // marketBuyOrders - case 0xa3e2038000000000000000000000000000000000000000000000000000000000 { recordAddressesForMarketFillVariant() } // marketBuyOrdersNoThrow - case 0x7e1d980800000000000000000000000000000000000000000000000000000000 { recordAddressesForMarketFillVariant() } // marketSellOrders - case 0xdd1c7d1800000000000000000000000000000000000000000000000000000000 { recordAddressesForMarketFillVariant() } // marketSellOrdersNoThrow - case 0x3c28d86100000000000000000000000000000000000000000000000000000000 { recordAddressesForMatchOrders() } // matchOrders - case 0xd46b02c300000000000000000000000000000000000000000000000000000000 {} // cancelOrder - case 0x4ac1478200000000000000000000000000000000000000000000000000000000 {} // batchCancelOrders - case 0x4f9559b100000000000000000000000000000000000000000000000000000000 {} // cancelOrdersUpTo - default { - // Revert with `Error("INVALID_OR_BLOCKED_EXCHANGE_SELECTOR")` - mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) - mstore(0x20, 0x0000002000000000000000000000000000000000000000000000000000000000) - mstore(0x40, 0x00000024494e56414c49445f4f525f424c4f434b45445f45584348414e47455f) - mstore(0x60, 0x53454c4543544f52000000000000000000000000000000000000000000000000) - mstore(0x80, 0x00000000) - // Revert length calculation: - // 4 -- error selector - // 32 -- offset to string - // 32 -- string length field - // 64 -- strlen(INVALID_OR_BLOCKED_EXCHANGE_SELECTOR) rounded up to nearest 32-byte word. - revert(0, 132) - } - - ///// Validate Recorded Addresses ///// - - // Load from memory the addresses to validate - let addressesToValidate := mload(0x40) - let addressesToValidateLength := mload(addressesToValidate) - let addressesToValidateElementPtr := add(addressesToValidate, 0x20) - let addressesToValidateElementEndPtr := add(addressesToValidateElementPtr, mul(addressesToValidateLength, 0x20)) - - // Set free memory pointer to after `addressesToValidate` array. - // This is to avoid corruption when making calls in the loop below. - let freeMemPtr := addressesToValidateElementEndPtr - mstore(0x40, freeMemPtr) - - // Validate addresses - let complianceTokenAddress := sload(COMPLIANCE_TOKEN_slot) - for {let addressToValidate := addressesToValidateElementPtr} lt(addressToValidate, addressesToValidateElementEndPtr) {addressToValidate := add(addressToValidate, 0x20)} { - // Construct calldata for `COMPLIANCE_TOKEN.balanceOf` - mstore(freeMemPtr, 0x70a0823100000000000000000000000000000000000000000000000000000000) - mstore(add(4, freeMemPtr), mload(addressToValidate)) - - // call `COMPLIANCE_TOKEN.balanceOf` - let success := call( - gas, // forward all gas - complianceTokenAddress, // call address of asset proxy - 0, // don't send any ETH - freeMemPtr, // pointer to start of input - 0x24, // length of input (one padded address) - freeMemPtr, // write output to next free memory offset - 0x20 // reserve space for return balance (0x20 bytes) - ) - if eq(success, 0) { - // @TODO Revert with `Error("BALANCE_QUERY_FAILED")` - mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) - mstore(0x20, 0x0000002000000000000000000000000000000000000000000000000000000000) - mstore(0x40, 0x0000001442414c414e43455f51554552595f4641494c45440000000000000000) - mstore(0x60, 0x00000000) - // Revert length calculation: - // 4 -- error selector - // 32 -- offset to string - // 32 -- string length field - // 32 -- strlen(BALANCE_QUERY_FAILED) rounded up to nearest 32-byte word. - revert(0, 100) - } - - // Revert if balance not held - let addressBalance := mload(freeMemPtr) - if eq(addressBalance, 0) { - // Revert with `Error("AT_LEAST_ONE_ADDRESS_DOES_NOT_MEET_BALANCE_THRESHOLD")` - mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) - mstore(0x20, 0x0000002000000000000000000000000000000000000000000000000000000000) - mstore(0x40, 0x0000003441545f4c454153545f4f4e455f414444524553535f444f45535f4e4f) - mstore(0x60, 0x545f4d4545545f42414c414e43455f5448524553484f4c440000000000000000) - mstore(0x80, 0x00000000) - // Revert length calculation: - // 4 -- error selector - // 32 -- offset to string - // 32 -- string length field - // 64 -- strlen(AT_LEAST_ONE_ADDRESS_DOES_NOT_MEET_BALANCE_THRESHOLD) rounded up to nearest 32-byte word. - revert(0, 132) - } - } - - // Record validated addresses - validatedAddresses := addressesToValidate - } - - ///// If we hit this point then all addresses are valid ///// - emit ValidatedAddresses(validatedAddresses); - - // All addresses are valid. Execute fillOrder. - EXCHANGE.executeTransaction( - salt, - signerAddress, - signedExchangeTransaction, - signature - ); - } -} \ No newline at end of file diff --git a/packages/contracts/test/extensions/balance_threshold_filter.ts b/packages/contracts/test/extensions/balance_threshold_filter.ts new file mode 100644 index 000000000..50fd79439 --- /dev/null +++ b/packages/contracts/test/extensions/balance_threshold_filter.ts @@ -0,0 +1,366 @@ +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { RevertReason, SignedOrder } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import * as chai from 'chai'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated-wrappers/dummy_erc20_token'; +import { ExchangeContract } from '../../generated-wrappers/exchange'; +import { BalanceThresholdFilterContract } from '../../generated-wrappers/balance_threshold_filter'; +import { YesComplianceTokenContract } from '../../generated-wrappers/yes_compliance_token'; + +import { artifacts } from '../../src/artifacts'; +import { + expectTransactionFailedAsync, + expectTransactionFailedWithoutReasonAsync, +} from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ExchangeWrapper } from '../utils/exchange_wrapper'; +import { OrderFactory } from '../utils/order_factory'; +import { orderUtils } from '../utils/order_utils'; +import { TransactionFactory } from '../utils/transaction_factory'; +import { ContractName, ERC20BalancesByOwner, SignedTransaction } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +import { MethodAbi, AbiDefinition } from 'ethereum-types'; +import { AbiEncoder } from '@0x/utils'; +import { Method } from '@0x/utils/lib/src/abi_encoder'; +import { LogDecoder } from '../utils/log_decoder'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +const DECIMALS_DEFAULT = 18; + +describe.only(ContractName.BalanceThresholdFilter, () => { + let compliantMakerAddress: string; + let owner: string; + let compliantTakerAddress: string; + let feeRecipientAddress: string; + let nonCompliantAddress: string; + let defaultMakerAssetAddress: string; + let defaultTakerAssetAddress: string; + let zrxAssetData: string; + let zrxToken: DummyERC20TokenContract; + let exchangeInstance: ExchangeContract; + let exchangeWrapper: ExchangeWrapper; + + let orderFactory: OrderFactory; + let erc20Wrapper: ERC20Wrapper; + let erc20Balances: ERC20BalancesByOwner; + + let takerTransactionFactory: TransactionFactory; + let compliantSignedOrder: SignedOrder; + let compliantSignedFillOrderTx: SignedTransaction; + let noncompliantSignedFillOrderTx: SignedTransaction; + + const takerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(500), DECIMALS_DEFAULT); + const makerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), DECIMALS_DEFAULT); + const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(250), DECIMALS_DEFAULT); + + let compliantForwarderInstance: BalanceThresholdFilterContract; + + before(async () => { + // Create accounts + await blockchainLifecycle.startAsync(); + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([ + owner, + compliantMakerAddress, + compliantTakerAddress, + feeRecipientAddress, + nonCompliantAddress, + ] = accounts); + // Create wrappers + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + // Deploy ERC20 tokens + const numDummyErc20ToDeploy = 3; + let erc20TokenA: DummyERC20TokenContract; + let erc20TokenB: DummyERC20TokenContract; + [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, + ); + defaultMakerAssetAddress = erc20TokenA.address; + defaultTakerAssetAddress = erc20TokenB.address; + zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + // Deploy Yes Token + const yesTokenInstance = await YesComplianceTokenContract.deployFrom0xArtifactAsync( + artifacts.YesComplianceToken, + provider, + txDefaults, + ); + // Create proxies + const erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + // Deploy Exchange congtract + exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + exchangeWrapper = new ExchangeWrapper(exchangeInstance, provider); + // Register proxies + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { + from: owner, + }); + // Default order parameters + const defaultOrderParams = { + exchangeAddress: exchangeInstance.address, + makerAddress: compliantMakerAddress, + feeRecipientAddress, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), + makerAssetAmount, + takerAssetAmount, + makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), DECIMALS_DEFAULT), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(150), DECIMALS_DEFAULT), + }; + const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(compliantMakerAddress)]; + orderFactory = new OrderFactory(privateKey, defaultOrderParams); + // Deploy Compliant Forwarder + compliantForwarderInstance = await BalanceThresholdFilterContract.deployFrom0xArtifactAsync( + artifacts.BalanceThresholdFilter, + provider, + txDefaults, + exchangeInstance.address, + yesTokenInstance.address, + ); + /* + const compliantForwarderContract = new BalanceThresholdFilterContract( + compliantForwarderInstance.abi, + compliantForwarderInstance.address, + provider, + ); + forwarderWrapper = new ForwarderWrapper(compliantForwarderContract, provider); + */ + // Initialize Yes Token + await yesTokenInstance._upgradeable_initialize.sendTransactionAsync({ from: owner }); + const yesTokenName = 'YesToken'; + const yesTokenTicker = 'YEET'; + await yesTokenInstance.initialize.sendTransactionAsync(yesTokenName, yesTokenTicker, { from: owner }); + // Verify Maker / Taker + const addressesCanControlTheirToken = true; + const compliantMakerCountryCode = new BigNumber(519); + const compliantMakerYesMark = new BigNumber(1); + const compliantMakerEntityId = new BigNumber(2); + await yesTokenInstance.mint2.sendTransactionAsync( + compliantMakerAddress, + compliantMakerEntityId, + addressesCanControlTheirToken, + compliantMakerCountryCode, + [compliantMakerYesMark], + { from: owner }, + ); + const compliantTakerCountryCode = new BigNumber(519); + const compliantTakerYesMark = new BigNumber(1); + const compliantTakerEntityId = new BigNumber(2); + await yesTokenInstance.mint2.sendTransactionAsync( + compliantTakerAddress, + compliantTakerEntityId, + addressesCanControlTheirToken, + compliantTakerCountryCode, + [compliantTakerYesMark], + { from: owner }, + ); + // Create Valid/Invalid orders + const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(compliantTakerAddress)]; + takerTransactionFactory = new TransactionFactory(takerPrivateKey, exchangeInstance.address); + compliantSignedOrder = await orderFactory.newSignedOrderAsync({ + senderAddress: compliantForwarderInstance.address, + }); + const compliantSignedOrderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress( + compliantSignedOrder, + ); + const compliantSignedOrderWithoutExchangeAddressData = exchangeInstance.fillOrder.getABIEncodedTransactionData( + compliantSignedOrderWithoutExchangeAddress, + takerAssetFillAmount, + compliantSignedOrder.signature, + ); + compliantSignedFillOrderTx = takerTransactionFactory.newSignedTransaction( + compliantSignedOrderWithoutExchangeAddressData, + ); + + /* generate selectors for every exchange method + _.each(exchangeInstance.abi, (abiDefinition: AbiDefinition) => { + try { + const method = new Method(abiDefinition as MethodAbi); + console.log('\n', `// ${method.getDataItem().name}`); + console.log(`bytes4 constant ${method.getDataItem().name}Selector = ${method.getSelector()};`); + console.log(`bytes4 constant ${method.getDataItem().name}SelectorGenerator = byes4(keccak256('${method.getSignature()}'));`); + } catch(e) { + _.noop(); + } + });*/ + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe.only('fillOrder', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + }); + it('should transfer the correct amounts when maker and taker are compliant', async () => { + const txHash = await compliantForwarderInstance.executeTransaction.sendTransactionAsync( + compliantSignedFillOrderTx.salt, + compliantSignedFillOrderTx.signerAddress, + compliantSignedFillOrderTx.data, + compliantSignedFillOrderTx.signature, + ); + const decoder = new LogDecoder(web3Wrapper); + const tx = await decoder.getTxWithDecodedLogsAsync(txHash); + console.log(JSON.stringify(tx, null, 4)); + console.log('****** MAKER ADDRESS = ', compliantSignedOrder.makerAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount = takerAssetFillAmount + .times(compliantSignedOrder.makerAssetAmount) + .dividedToIntegerBy(compliantSignedOrder.takerAssetAmount); + const makerFeePaid = compliantSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(compliantSignedOrder.makerAssetAmount); + const takerFeePaid = compliantSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(compliantSignedOrder.makerAssetAmount); + expect(newBalances[compliantMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[compliantMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[compliantMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[compliantMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[compliantMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[compliantMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + expect(newBalances[compliantTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[compliantTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), + ); + expect(newBalances[compliantTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[compliantTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount), + ); + expect(newBalances[compliantTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[compliantTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), + ); + }); + it('should revert if the signed transaction is not intended for fillOrder', async () => { + // Create signed order without the fillOrder function selector + const txDataBuf = ethUtil.toBuffer(compliantSignedFillOrderTx.data); + const selectorLengthInBytes = 4; + const txDataBufMinusSelector = txDataBuf.slice(selectorLengthInBytes); + const badSelector = '0x00000000'; + const badSelectorBuf = ethUtil.toBuffer(badSelector); + const txDataBufWithBadSelector = Buffer.concat([badSelectorBuf, txDataBufMinusSelector]); + const txDataBufWithBadSelectorHex = ethUtil.bufferToHex(txDataBufWithBadSelector); + // Call compliant forwarder + return expectTransactionFailedWithoutReasonAsync(compliantForwarderInstance.executeTransaction.sendTransactionAsync( + compliantSignedFillOrderTx.salt, + compliantSignedFillOrderTx.signerAddress, + txDataBufWithBadSelectorHex, + compliantSignedFillOrderTx.signature, + )); + }); + it('should revert if senderAddress is not set to the compliant forwarding contract', async () => { + // Create signed order with incorrect senderAddress + const notBalanceThresholdFilterAddress = zrxToken.address; + const signedOrderWithBadSenderAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: notBalanceThresholdFilterAddress, + }); + const signedOrderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress( + signedOrderWithBadSenderAddress, + ); + const signedOrderWithoutExchangeAddressData = exchangeInstance.fillOrder.getABIEncodedTransactionData( + signedOrderWithoutExchangeAddress, + takerAssetFillAmount, + compliantSignedOrder.signature, + ); + const signedFillOrderTx = takerTransactionFactory.newSignedTransaction( + signedOrderWithoutExchangeAddressData, + ); + // Call compliant forwarder + return expectTransactionFailedWithoutReasonAsync(compliantForwarderInstance.executeTransaction.sendTransactionAsync( + signedFillOrderTx.salt, + signedFillOrderTx.signerAddress, + signedFillOrderTx.data, + signedFillOrderTx.signature, + )); + }); + it('should revert if taker address is not compliant (does not hold a Yes Token)', async () => { + return expectTransactionFailedAsync( + compliantForwarderInstance.executeTransaction.sendTransactionAsync( + compliantSignedFillOrderTx.salt, + nonCompliantAddress, + compliantSignedFillOrderTx.data, + compliantSignedFillOrderTx.signature, + ), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold + ); + }); + it('should revert if maker address is not compliant (does not hold a Yes Token)', async () => { + // Create signed order with non-compliant maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: compliantForwarderInstance.address, + makerAddress: nonCompliantAddress + }); + const signedOrderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress( + signedOrderWithBadMakerAddress, + ); + const signedOrderWithoutExchangeAddressData = exchangeInstance.fillOrder.getABIEncodedTransactionData( + signedOrderWithoutExchangeAddress, + takerAssetFillAmount, + compliantSignedOrder.signature, + ); + const signedFillOrderTx = takerTransactionFactory.newSignedTransaction( + signedOrderWithoutExchangeAddressData, + ); + // Call compliant forwarder + return expectTransactionFailedAsync( + compliantForwarderInstance.executeTransaction.sendTransactionAsync( + signedFillOrderTx.salt, + signedFillOrderTx.signerAddress, + signedFillOrderTx.data, + signedFillOrderTx.signature, + ), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold + ); + }); + }); + + describe('batchFillOrders', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + }); + it('should transfer the correct amounts when maker and taker are compliant', async () => { + let order2 = _.cloneDeep(compliantSignedOrder); + order2.makerAddress = `0x${_.reverse(compliantSignedOrder.makerAddress.slice(2).split('')).join('')}`; + const orders = [compliantSignedOrder, order2]; + const fillAmounts = [new BigNumber(4), new BigNumber(4)]; + const signatures = ["0xabcd", "0xabcd"]; + const exchangeCalldata = exchangeInstance.batchFillOrders.getABIEncodedTransactionData(orders, fillAmounts, signatures); + console.log('*'.repeat(40), exchangeCalldata, '*'.repeat(40)); + console.log('****** MAKER ADDRESS = ', compliantSignedOrder.makerAddress); + + const txHash = await compliantForwarderInstance.executeTransaction.sendTransactionAsync( + compliantSignedFillOrderTx.salt, + compliantSignedFillOrderTx.signerAddress, + exchangeCalldata, + compliantSignedFillOrderTx.signature, + ); + const decoder = new LogDecoder(web3Wrapper); + const tx = await decoder.getTxWithDecodedLogsAsync(txHash); + console.log(JSON.stringify(tx, null, 4)); + }); + }); +}); +// tslint:disable:max-file-line-count +// tslint:enable:no-unnecessary-type-assertion diff --git a/packages/contracts/test/extensions/compliant_forwarder.ts b/packages/contracts/test/extensions/compliant_forwarder.ts deleted file mode 100644 index 196167264..000000000 --- a/packages/contracts/test/extensions/compliant_forwarder.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { BlockchainLifecycle } from '@0x/dev-utils'; -import { assetDataUtils } from '@0x/order-utils'; -import { RevertReason, SignedOrder } from '@0x/types'; -import { BigNumber } from '@0x/utils'; -import { Web3Wrapper } from '@0x/web3-wrapper'; -import * as chai from 'chai'; -import * as ethUtil from 'ethereumjs-util'; -import * as _ from 'lodash'; - -import { DummyERC20TokenContract } from '../../generated-wrappers/dummy_erc20_token'; -import { ExchangeContract } from '../../generated-wrappers/exchange'; -import { CompliantForwarderContract } from '../../generated-wrappers/compliant_forwarder'; -import { YesComplianceTokenContract } from '../../generated-wrappers/yes_compliance_token'; - -import { artifacts } from '../../src/artifacts'; -import { - expectTransactionFailedAsync, - expectTransactionFailedWithoutReasonAsync, -} from '../utils/assertions'; -import { chaiSetup } from '../utils/chai_setup'; -import { constants } from '../utils/constants'; -import { ERC20Wrapper } from '../utils/erc20_wrapper'; -import { ExchangeWrapper } from '../utils/exchange_wrapper'; -import { OrderFactory } from '../utils/order_factory'; -import { orderUtils } from '../utils/order_utils'; -import { TransactionFactory } from '../utils/transaction_factory'; -import { ContractName, ERC20BalancesByOwner, SignedTransaction } from '../utils/types'; -import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; - -import { MethodAbi, AbiDefinition } from 'ethereum-types'; -import { AbiEncoder } from '@0x/utils'; -import { Method } from '@0x/utils/lib/src/abi_encoder'; -import { LogDecoder } from '../utils/log_decoder'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); -const DECIMALS_DEFAULT = 18; - -describe.only(ContractName.CompliantForwarder, () => { - let compliantMakerAddress: string; - let owner: string; - let compliantTakerAddress: string; - let feeRecipientAddress: string; - let nonCompliantAddress: string; - let defaultMakerAssetAddress: string; - let defaultTakerAssetAddress: string; - let zrxAssetData: string; - let zrxToken: DummyERC20TokenContract; - let exchangeInstance: ExchangeContract; - let exchangeWrapper: ExchangeWrapper; - - let orderFactory: OrderFactory; - let erc20Wrapper: ERC20Wrapper; - let erc20Balances: ERC20BalancesByOwner; - - let takerTransactionFactory: TransactionFactory; - let compliantSignedOrder: SignedOrder; - let compliantSignedFillOrderTx: SignedTransaction; - let noncompliantSignedFillOrderTx: SignedTransaction; - - const takerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(500), DECIMALS_DEFAULT); - const makerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), DECIMALS_DEFAULT); - const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(250), DECIMALS_DEFAULT); - - let compliantForwarderInstance: CompliantForwarderContract; - - before(async () => { - // Create accounts - await blockchainLifecycle.startAsync(); - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - const usedAddresses = ([ - owner, - compliantMakerAddress, - compliantTakerAddress, - feeRecipientAddress, - nonCompliantAddress, - ] = accounts); - // Create wrappers - erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); - // Deploy ERC20 tokens - const numDummyErc20ToDeploy = 3; - let erc20TokenA: DummyERC20TokenContract; - let erc20TokenB: DummyERC20TokenContract; - [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( - numDummyErc20ToDeploy, - constants.DUMMY_TOKEN_DECIMALS, - ); - defaultMakerAssetAddress = erc20TokenA.address; - defaultTakerAssetAddress = erc20TokenB.address; - zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - // Deploy Yes Token - const yesTokenInstance = await YesComplianceTokenContract.deployFrom0xArtifactAsync( - artifacts.YesComplianceToken, - provider, - txDefaults, - ); - // Create proxies - const erc20Proxy = await erc20Wrapper.deployProxyAsync(); - await erc20Wrapper.setBalancesAndAllowancesAsync(); - // Deploy Exchange congtract - exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( - artifacts.Exchange, - provider, - txDefaults, - zrxAssetData, - ); - exchangeWrapper = new ExchangeWrapper(exchangeInstance, provider); - // Register proxies - await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); - await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { - from: owner, - }); - // Default order parameters - const defaultOrderParams = { - exchangeAddress: exchangeInstance.address, - makerAddress: compliantMakerAddress, - feeRecipientAddress, - makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), - takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), - makerAssetAmount, - takerAssetAmount, - makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), DECIMALS_DEFAULT), - takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(150), DECIMALS_DEFAULT), - }; - const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(compliantMakerAddress)]; - orderFactory = new OrderFactory(privateKey, defaultOrderParams); - // Deploy Compliant Forwarder - compliantForwarderInstance = await CompliantForwarderContract.deployFrom0xArtifactAsync( - artifacts.CompliantForwarder, - provider, - txDefaults, - exchangeInstance.address, - yesTokenInstance.address, - ); - /* - const compliantForwarderContract = new CompliantForwarderContract( - compliantForwarderInstance.abi, - compliantForwarderInstance.address, - provider, - ); - forwarderWrapper = new ForwarderWrapper(compliantForwarderContract, provider); - */ - // Initialize Yes Token - await yesTokenInstance._upgradeable_initialize.sendTransactionAsync({ from: owner }); - const yesTokenName = 'YesToken'; - const yesTokenTicker = 'YEET'; - await yesTokenInstance.initialize.sendTransactionAsync(yesTokenName, yesTokenTicker, { from: owner }); - // Verify Maker / Taker - const addressesCanControlTheirToken = true; - const compliantMakerCountryCode = new BigNumber(519); - const compliantMakerYesMark = new BigNumber(1); - const compliantMakerEntityId = new BigNumber(2); - await yesTokenInstance.mint2.sendTransactionAsync( - compliantMakerAddress, - compliantMakerEntityId, - addressesCanControlTheirToken, - compliantMakerCountryCode, - [compliantMakerYesMark], - { from: owner }, - ); - const compliantTakerCountryCode = new BigNumber(519); - const compliantTakerYesMark = new BigNumber(1); - const compliantTakerEntityId = new BigNumber(2); - await yesTokenInstance.mint2.sendTransactionAsync( - compliantTakerAddress, - compliantTakerEntityId, - addressesCanControlTheirToken, - compliantTakerCountryCode, - [compliantTakerYesMark], - { from: owner }, - ); - // Create Valid/Invalid orders - const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(compliantTakerAddress)]; - takerTransactionFactory = new TransactionFactory(takerPrivateKey, exchangeInstance.address); - compliantSignedOrder = await orderFactory.newSignedOrderAsync({ - senderAddress: compliantForwarderInstance.address, - }); - const compliantSignedOrderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress( - compliantSignedOrder, - ); - const compliantSignedOrderWithoutExchangeAddressData = exchangeInstance.fillOrder.getABIEncodedTransactionData( - compliantSignedOrderWithoutExchangeAddress, - takerAssetFillAmount, - compliantSignedOrder.signature, - ); - compliantSignedFillOrderTx = takerTransactionFactory.newSignedTransaction( - compliantSignedOrderWithoutExchangeAddressData, - ); - - /* generate selectors for every exchange method - _.each(exchangeInstance.abi, (abiDefinition: AbiDefinition) => { - try { - const method = new Method(abiDefinition as MethodAbi); - console.log('\n', `// ${method.getDataItem().name}`); - console.log(`bytes4 constant ${method.getDataItem().name}Selector = ${method.getSelector()};`); - console.log(`bytes4 constant ${method.getDataItem().name}SelectorGenerator = byes4(keccak256('${method.getSignature()}'));`); - } catch(e) { - _.noop(); - } - });*/ - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe.only('fillOrder', () => { - beforeEach(async () => { - erc20Balances = await erc20Wrapper.getBalancesAsync(); - }); - it('should transfer the correct amounts when maker and taker are compliant', async () => { - const txHash = await compliantForwarderInstance.executeTransaction.sendTransactionAsync( - compliantSignedFillOrderTx.salt, - compliantSignedFillOrderTx.signerAddress, - compliantSignedFillOrderTx.data, - compliantSignedFillOrderTx.signature, - ); - const decoder = new LogDecoder(web3Wrapper); - const tx = await decoder.getTxWithDecodedLogsAsync(txHash); - console.log(JSON.stringify(tx, null, 4)); - console.log('****** MAKER ADDRESS = ', compliantSignedOrder.makerAddress); - const newBalances = await erc20Wrapper.getBalancesAsync(); - const makerAssetFillAmount = takerAssetFillAmount - .times(compliantSignedOrder.makerAssetAmount) - .dividedToIntegerBy(compliantSignedOrder.takerAssetAmount); - const makerFeePaid = compliantSignedOrder.makerFee - .times(makerAssetFillAmount) - .dividedToIntegerBy(compliantSignedOrder.makerAssetAmount); - const takerFeePaid = compliantSignedOrder.takerFee - .times(makerAssetFillAmount) - .dividedToIntegerBy(compliantSignedOrder.makerAssetAmount); - expect(newBalances[compliantMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[compliantMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), - ); - expect(newBalances[compliantMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[compliantMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), - ); - expect(newBalances[compliantMakerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[compliantMakerAddress][zrxToken.address].minus(makerFeePaid), - ); - expect(newBalances[compliantTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[compliantTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), - ); - expect(newBalances[compliantTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( - erc20Balances[compliantTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount), - ); - expect(newBalances[compliantTakerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[compliantTakerAddress][zrxToken.address].minus(takerFeePaid), - ); - expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), - ); - }); - it('should revert if the signed transaction is not intended for fillOrder', async () => { - // Create signed order without the fillOrder function selector - const txDataBuf = ethUtil.toBuffer(compliantSignedFillOrderTx.data); - const selectorLengthInBytes = 4; - const txDataBufMinusSelector = txDataBuf.slice(selectorLengthInBytes); - const badSelector = '0x00000000'; - const badSelectorBuf = ethUtil.toBuffer(badSelector); - const txDataBufWithBadSelector = Buffer.concat([badSelectorBuf, txDataBufMinusSelector]); - const txDataBufWithBadSelectorHex = ethUtil.bufferToHex(txDataBufWithBadSelector); - // Call compliant forwarder - return expectTransactionFailedWithoutReasonAsync(compliantForwarderInstance.executeTransaction.sendTransactionAsync( - compliantSignedFillOrderTx.salt, - compliantSignedFillOrderTx.signerAddress, - txDataBufWithBadSelectorHex, - compliantSignedFillOrderTx.signature, - )); - }); - it('should revert if senderAddress is not set to the compliant forwarding contract', async () => { - // Create signed order with incorrect senderAddress - const notCompliantForwarderAddress = zrxToken.address; - const signedOrderWithBadSenderAddress = await orderFactory.newSignedOrderAsync({ - senderAddress: notCompliantForwarderAddress, - }); - const signedOrderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress( - signedOrderWithBadSenderAddress, - ); - const signedOrderWithoutExchangeAddressData = exchangeInstance.fillOrder.getABIEncodedTransactionData( - signedOrderWithoutExchangeAddress, - takerAssetFillAmount, - compliantSignedOrder.signature, - ); - const signedFillOrderTx = takerTransactionFactory.newSignedTransaction( - signedOrderWithoutExchangeAddressData, - ); - // Call compliant forwarder - return expectTransactionFailedWithoutReasonAsync(compliantForwarderInstance.executeTransaction.sendTransactionAsync( - signedFillOrderTx.salt, - signedFillOrderTx.signerAddress, - signedFillOrderTx.data, - signedFillOrderTx.signature, - )); - }); - it('should revert if taker address is not compliant (does not hold a Yes Token)', async () => { - return expectTransactionFailedAsync( - compliantForwarderInstance.executeTransaction.sendTransactionAsync( - compliantSignedFillOrderTx.salt, - nonCompliantAddress, - compliantSignedFillOrderTx.data, - compliantSignedFillOrderTx.signature, - ), - RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold - ); - }); - it('should revert if maker address is not compliant (does not hold a Yes Token)', async () => { - // Create signed order with non-compliant maker address - const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ - senderAddress: compliantForwarderInstance.address, - makerAddress: nonCompliantAddress - }); - const signedOrderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress( - signedOrderWithBadMakerAddress, - ); - const signedOrderWithoutExchangeAddressData = exchangeInstance.fillOrder.getABIEncodedTransactionData( - signedOrderWithoutExchangeAddress, - takerAssetFillAmount, - compliantSignedOrder.signature, - ); - const signedFillOrderTx = takerTransactionFactory.newSignedTransaction( - signedOrderWithoutExchangeAddressData, - ); - // Call compliant forwarder - return expectTransactionFailedAsync( - compliantForwarderInstance.executeTransaction.sendTransactionAsync( - signedFillOrderTx.salt, - signedFillOrderTx.signerAddress, - signedFillOrderTx.data, - signedFillOrderTx.signature, - ), - RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold - ); - }); - }); - - describe('batchFillOrders', () => { - beforeEach(async () => { - erc20Balances = await erc20Wrapper.getBalancesAsync(); - }); - it('should transfer the correct amounts when maker and taker are compliant', async () => { - let order2 = _.cloneDeep(compliantSignedOrder); - order2.makerAddress = `0x${_.reverse(compliantSignedOrder.makerAddress.slice(2).split('')).join('')}`; - const orders = [compliantSignedOrder, order2]; - const fillAmounts = [new BigNumber(4), new BigNumber(4)]; - const signatures = ["0xabcd", "0xabcd"]; - const exchangeCalldata = exchangeInstance.batchFillOrders.getABIEncodedTransactionData(orders, fillAmounts, signatures); - console.log('*'.repeat(40), exchangeCalldata, '*'.repeat(40)); - console.log('****** MAKER ADDRESS = ', compliantSignedOrder.makerAddress); - - const txHash = await compliantForwarderInstance.executeTransaction.sendTransactionAsync( - compliantSignedFillOrderTx.salt, - compliantSignedFillOrderTx.signerAddress, - exchangeCalldata, - compliantSignedFillOrderTx.signature, - ); - const decoder = new LogDecoder(web3Wrapper); - const tx = await decoder.getTxWithDecodedLogsAsync(txHash); - console.log(JSON.stringify(tx, null, 4)); - }); - }); -}); -// tslint:disable:max-file-line-count -// tslint:enable:no-unnecessary-type-assertion -- cgit v1.2.3