├── .gitignore ├── contracts ├── Migrations.sol ├── ZeroExFeeWrapper.sol ├── lib │ ├── ArrayUtils.sol │ ├── LibFillResults.sol │ ├── LibOrder.sol │ └── ReentrancyGuard.sol └── test │ ├── ExchangeMock.sol │ ├── TestERC20.sol │ └── TestERC721.sol ├── migrations └── 1_initial_migration.js ├── package-lock.json ├── package.json ├── test ├── 0-exchange-mock.js ├── 1-fee-exchange-wrapper.js └── util.js └── truffle-config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.4.22 <0.9.0; 3 | 4 | contract Migrations { 5 | address public owner = msg.sender; 6 | uint public last_completed_migration; 7 | 8 | modifier restricted() { 9 | require( 10 | msg.sender == owner, 11 | "This function is restricted to the contract's owner" 12 | ); 13 | _; 14 | } 15 | 16 | function setCompleted(uint completed) public restricted { 17 | last_completed_migration = completed; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/ZeroExFeeWrapper.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.0; 2 | pragma abicoder v2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import "./lib/ReentrancyGuard.sol"; 7 | import "./lib/LibOrder.sol"; 8 | 9 | contract ZeroExFeeWrapper is ReentrancyGuard { 10 | 11 | mapping(address => bool) _owners; 12 | address _exchange; 13 | 14 | struct FeeData { 15 | address recipient; 16 | uint256 paymentTokenAmount; 17 | } 18 | 19 | constructor(address exchange) 20 | { 21 | _owners[msg.sender] = true; 22 | _exchange = exchange; 23 | } 24 | 25 | modifier ownerOnly() 26 | { 27 | require(_owners[msg.sender],"Owner only"); 28 | _; 29 | } 30 | 31 | modifier refundRemainingBalance() 32 | { 33 | _; 34 | uint256 balance = address(this).balance; 35 | if (balance > 0) { 36 | payable(msg.sender).transfer(balance); 37 | } 38 | } 39 | 40 | function setOwner(address owner,bool isOwner) 41 | external 42 | ownerOnly 43 | { 44 | _owners[owner] = isOwner; 45 | } 46 | 47 | function isOwner(address owner) 48 | external 49 | view 50 | returns (bool) 51 | { 52 | return _owners[owner]; 53 | } 54 | 55 | function setExchange(address exchange) 56 | external 57 | ownerOnly 58 | { 59 | _exchange = exchange; 60 | } 61 | 62 | function getExchange() 63 | external 64 | view 65 | returns (address) 66 | { 67 | return _exchange; 68 | } 69 | 70 | function proxyCall(address target,bytes calldata callData) 71 | external 72 | payable 73 | ownerOnly 74 | returns (bytes memory) 75 | { 76 | (bool success, bytes memory result) = target.call{value: msg.value}(callData); 77 | return result; 78 | } 79 | 80 | function matchOrders( 81 | LibOrder.Order memory leftOrder, 82 | LibOrder.Order memory rightOrder, 83 | bytes memory leftSignature, 84 | bytes memory rightSignature, 85 | FeeData[] memory feeData, 86 | address paymentTokenAddress 87 | ) 88 | external 89 | payable 90 | ownerOnly 91 | reentrancyGuard 92 | refundRemainingBalance 93 | returns (bytes memory) 94 | { 95 | if (leftOrder.senderAddress != address(0x0)) { 96 | require(leftOrder.senderAddress == address(this),"leftOrder.senderAddress has to be 0x0 or the wrapper address"); 97 | } 98 | if (rightOrder.senderAddress != address(0x0)) { 99 | require(rightOrder.senderAddress == address(this),"rightOrder.senderAddress has to be 0x0 or the wrapper address"); 100 | } 101 | bool transferFees = paymentTokenAddress != address(0x0) && feeData.length > 0; 102 | uint256 currentFeeBalance; 103 | if (transferFees) { 104 | require(leftOrder.feeRecipientAddress == address(this) || rightOrder.feeRecipientAddress == address(this),"Neither order has a fee recipient"); 105 | currentFeeBalance = ERC20(paymentTokenAddress).balanceOf(address(this)); 106 | } 107 | (bool success, bytes memory result) = _exchange.call{value: msg.value}(abi.encodeWithSignature("matchOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes),(address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes),bytes,bytes)",leftOrder,rightOrder,leftSignature,rightSignature)); 108 | require(success,"matchOrders failed"); 109 | if (transferFees) { 110 | for (uint index = 0 ; index < feeData.length ; ++index) { 111 | SafeERC20.safeTransfer(ERC20(paymentTokenAddress),feeData[index].recipient, feeData[index].paymentTokenAmount); 112 | } 113 | require(ERC20(paymentTokenAddress).balanceOf(address(this)) == currentFeeBalance,"Did not transfer the exact payment fee amount"); 114 | } 115 | return result; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /contracts/lib/ArrayUtils.sol: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | << ArrayUtils >> 4 | 5 | Various functions for manipulating arrays in Solidity. 6 | This library is completely inlined and does not need to be deployed or linked. 7 | 8 | */ 9 | 10 | pragma solidity 0.8.0; 11 | 12 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 13 | 14 | /** 15 | * @title ArrayUtils 16 | * @author Wyvern Protocol Developers 17 | */ 18 | library ArrayUtils { 19 | 20 | /** 21 | * Replace bytes in an array with bytes in another array, guarded by a bitmask 22 | * Efficiency of this function is a bit unpredictable because of the EVM's word-specific model (arrays under 32 bytes will be slower) 23 | * Modifies the provided byte array parameter in place 24 | * 25 | * @dev Mask must be the size of the byte array. A nonzero byte means the byte array can be changed. 26 | * @param array The original array 27 | * @param desired The target array 28 | * @param mask The mask specifying which bits can be changed 29 | */ 30 | function guardedArrayReplace(bytes memory array, bytes memory desired, bytes memory mask) 31 | internal 32 | pure 33 | { 34 | require(array.length == desired.length, "Arrays have different lengths"); 35 | require(array.length == mask.length, "Array and mask have different lengths"); 36 | 37 | uint words = array.length / 0x20; 38 | uint index = words * 0x20; 39 | assert(index / 0x20 == words); 40 | uint i; 41 | 42 | for (i = 0; i < words; i++) { 43 | /* Conceptually: array[i] = (!mask[i] && array[i]) || (mask[i] && desired[i]), bitwise in word chunks. */ 44 | assembly { 45 | let commonIndex := mul(0x20, add(1, i)) 46 | let maskValue := mload(add(mask, commonIndex)) 47 | mstore(add(array, commonIndex), or(and(not(maskValue), mload(add(array, commonIndex))), and(maskValue, mload(add(desired, commonIndex))))) 48 | } 49 | } 50 | 51 | /* Deal with the last section of the byte array. */ 52 | if (words > 0) { 53 | /* This overlaps with bytes already set but is still more efficient than iterating through each of the remaining bytes individually. */ 54 | i = words; 55 | assembly { 56 | let commonIndex := mul(0x20, add(1, i)) 57 | let maskValue := mload(add(mask, commonIndex)) 58 | mstore(add(array, commonIndex), or(and(not(maskValue), mload(add(array, commonIndex))), and(maskValue, mload(add(desired, commonIndex))))) 59 | } 60 | } else { 61 | /* If the byte array is shorter than a word, we must unfortunately do the whole thing bytewise. 62 | (bounds checks could still probably be optimized away in assembly, but this is a rare case) */ 63 | for (i = index; i < array.length; i++) { 64 | array[i] = ((mask[i] ^ 0xff) & array[i]) | (mask[i] & desired[i]); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Test if two arrays are equal 71 | * Source: https://github.com/GNSPS/solidity-bytes-utils/blob/master/contracts/BytesLib.sol 72 | * 73 | * @dev Arrays must be of equal length, otherwise will return false 74 | * @param a First array 75 | * @param b Second array 76 | * @return Whether or not all bytes in the arrays are equal 77 | */ 78 | function arrayEq(bytes memory a, bytes memory b) 79 | internal 80 | pure 81 | returns (bool) 82 | { 83 | bool success = true; 84 | 85 | assembly { 86 | let length := mload(a) 87 | 88 | // if lengths don't match the arrays are not equal 89 | switch eq(length, mload(b)) 90 | case 1 { 91 | // cb is a circuit breaker in the for loop since there's 92 | // no said feature for inline assembly loops 93 | // cb = 1 - don't breaker 94 | // cb = 0 - break 95 | let cb := 1 96 | 97 | let mc := add(a, 0x20) 98 | let end := add(mc, length) 99 | 100 | for { 101 | let cc := add(b, 0x20) 102 | // the next line is the loop condition: 103 | // while(uint(mc < end) + cb == 2) 104 | } eq(add(lt(mc, end), cb), 2) { 105 | mc := add(mc, 0x20) 106 | cc := add(cc, 0x20) 107 | } { 108 | // if any of these checks fails then arrays are not equal 109 | if iszero(eq(mload(mc), mload(cc))) { 110 | // unsuccess: 111 | success := 0 112 | cb := 0 113 | } 114 | } 115 | } 116 | default { 117 | // unsuccess: 118 | success := 0 119 | } 120 | } 121 | 122 | return success; 123 | } 124 | 125 | /** 126 | * Drop the beginning of an array 127 | * 128 | * @param _bytes array 129 | * @param _start start index 130 | * @return Whether or not all bytes in the arrays are equal 131 | */ 132 | function arrayDrop(bytes memory _bytes, uint _start) 133 | internal 134 | pure 135 | returns (bytes memory) 136 | { 137 | 138 | uint _length = SafeMath.sub(_bytes.length, _start); 139 | return arraySlice(_bytes, _start, _length); 140 | } 141 | 142 | /** 143 | * Take from the beginning of an array 144 | * 145 | * @param _bytes array 146 | * @param _length elements to take 147 | * @return Whether or not all bytes in the arrays are equal 148 | */ 149 | function arrayTake(bytes memory _bytes, uint _length) 150 | internal 151 | pure 152 | returns (bytes memory) 153 | { 154 | 155 | return arraySlice(_bytes, 0, _length); 156 | } 157 | 158 | /** 159 | * Slice an array 160 | * Source: https://github.com/GNSPS/solidity-bytes-utils/blob/master/contracts/BytesLib.sol 161 | * 162 | * @param _bytes array 163 | * @param _start start index 164 | * @param _length length to take 165 | * @return Whether or not all bytes in the arrays are equal 166 | */ 167 | function arraySlice(bytes memory _bytes, uint _start, uint _length) 168 | internal 169 | pure 170 | returns (bytes memory) 171 | { 172 | 173 | bytes memory tempBytes; 174 | 175 | assembly { 176 | switch iszero(_length) 177 | case 0 { 178 | // Get a location of some free memory and store it in tempBytes as 179 | // Solidity does for memory variables. 180 | tempBytes := mload(0x40) 181 | 182 | // The first word of the slice result is potentially a partial 183 | // word read from the original array. To read it, we calculate 184 | // the length of that partial word and start copying that many 185 | // bytes into the array. The first word we copy will start with 186 | // data we don't care about, but the last `lengthmod` bytes will 187 | // land at the beginning of the contents of the new array. When 188 | // we're done copying, we overwrite the full first word with 189 | // the actual length of the slice. 190 | let lengthmod := and(_length, 31) 191 | 192 | // The multiplication in the next line is necessary 193 | // because when slicing multiples of 32 bytes (lengthmod == 0) 194 | // the following copy loop was copying the origin's length 195 | // and then ending prematurely not copying everything it should. 196 | let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) 197 | let end := add(mc, _length) 198 | 199 | for { 200 | // The multiplication in the next line has the same exact purpose 201 | // as the one above. 202 | let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) 203 | } lt(mc, end) { 204 | mc := add(mc, 0x20) 205 | cc := add(cc, 0x20) 206 | } { 207 | mstore(mc, mload(cc)) 208 | } 209 | 210 | mstore(tempBytes, _length) 211 | 212 | //update free-memory pointer 213 | //allocating the array padded to 32 bytes like the compiler does now 214 | mstore(0x40, and(add(mc, 31), not(31))) 215 | } 216 | //if we want a zero-length slice let's just return a zero-length array 217 | default { 218 | tempBytes := mload(0x40) 219 | 220 | mstore(0x40, add(tempBytes, 0x20)) 221 | } 222 | } 223 | 224 | return tempBytes; 225 | } 226 | 227 | /** 228 | * Unsafe write byte array into a memory location 229 | * 230 | * @param index Memory location 231 | * @param source Byte array to write 232 | * @return End memory index 233 | */ 234 | function unsafeWriteBytes(uint index, bytes memory source) 235 | internal 236 | pure 237 | returns (uint) 238 | { 239 | if (source.length > 0) { 240 | assembly { 241 | let length := mload(source) 242 | let end := add(source, add(0x20, length)) 243 | let arrIndex := add(source, 0x20) 244 | let tempIndex := index 245 | for { } eq(lt(arrIndex, end), 1) { 246 | arrIndex := add(arrIndex, 0x20) 247 | tempIndex := add(tempIndex, 0x20) 248 | } { 249 | mstore(tempIndex, mload(arrIndex)) 250 | } 251 | index := add(index, length) 252 | } 253 | } 254 | return index; 255 | } 256 | 257 | } 258 | -------------------------------------------------------------------------------- /contracts/lib/LibFillResults.sol: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2019 ZeroEx Intl. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | pragma solidity 0.8.0; 20 | 21 | import "./LibOrder.sol"; 22 | 23 | library LibFillResults { 24 | 25 | // using LibSafeMath for uint256; 26 | 27 | struct BatchMatchedFillResults { 28 | FillResults[] left; // Fill results for left orders 29 | FillResults[] right; // Fill results for right orders 30 | uint256 profitInLeftMakerAsset; // Profit taken from left makers 31 | uint256 profitInRightMakerAsset; // Profit taken from right makers 32 | } 33 | 34 | struct FillResults { 35 | uint256 makerAssetFilledAmount; // Total amount of makerAsset(s) filled. 36 | uint256 takerAssetFilledAmount; // Total amount of takerAsset(s) filled. 37 | uint256 makerFeePaid; // Total amount of fees paid by maker(s) to feeRecipient(s). 38 | uint256 takerFeePaid; // Total amount of fees paid by taker to feeRecipients(s). 39 | uint256 protocolFeePaid; // Total amount of fees paid by taker to the staking contract. 40 | } 41 | 42 | struct MatchedFillResults { 43 | FillResults left; // Amounts filled and fees paid of left order. 44 | FillResults right; // Amounts filled and fees paid of right order. 45 | uint256 profitInLeftMakerAsset; // Profit taken from the left maker 46 | uint256 profitInRightMakerAsset; // Profit taken from the right maker 47 | } 48 | 49 | /// @dev Calculates amounts filled and fees paid by maker and taker. 50 | /// @param order to be filled. 51 | /// @param takerAssetFilledAmount Amount of takerAsset that will be filled. 52 | /// @param protocolFeeMultiplier The current protocol fee of the exchange contract. 53 | /// @param gasPrice The gasprice of the transaction. This is provided so that the function call can continue 54 | /// to be pure rather than view. 55 | /// @return fillResults Amounts filled and fees paid by maker and taker. 56 | // function calculateFillResults( 57 | // LibOrder.Order memory order, 58 | // uint256 takerAssetFilledAmount, 59 | // uint256 protocolFeeMultiplier, 60 | // uint256 gasPrice 61 | // ) 62 | // internal 63 | // pure 64 | // returns (FillResults memory fillResults) 65 | // { 66 | // // Compute proportional transfer amounts 67 | // fillResults.takerAssetFilledAmount = takerAssetFilledAmount; 68 | // fillResults.makerAssetFilledAmount = LibMath.safeGetPartialAmountFloor( 69 | // takerAssetFilledAmount, 70 | // order.takerAssetAmount, 71 | // order.makerAssetAmount 72 | // ); 73 | // fillResults.makerFeePaid = LibMath.safeGetPartialAmountFloor( 74 | // takerAssetFilledAmount, 75 | // order.takerAssetAmount, 76 | // order.makerFee 77 | // ); 78 | // fillResults.takerFeePaid = LibMath.safeGetPartialAmountFloor( 79 | // takerAssetFilledAmount, 80 | // order.takerAssetAmount, 81 | // order.takerFee 82 | // ); 83 | 84 | // // Compute the protocol fee that should be paid for a single fill. 85 | // fillResults.protocolFeePaid = gasPrice.safeMul(protocolFeeMultiplier); 86 | 87 | // return fillResults; 88 | // } 89 | 90 | // /// @dev Calculates fill amounts for the matched orders. 91 | // /// Each order is filled at their respective price point. However, the calculations are 92 | // /// carried out as though the orders are both being filled at the right order's price point. 93 | // /// The profit made by the leftOrder order goes to the taker (who matched the two orders). 94 | // /// @param leftOrder First order to match. 95 | // /// @param rightOrder Second order to match. 96 | // /// @param leftOrderTakerAssetFilledAmount Amount of left order already filled. 97 | // /// @param rightOrderTakerAssetFilledAmount Amount of right order already filled. 98 | // /// @param protocolFeeMultiplier The current protocol fee of the exchange contract. 99 | // /// @param gasPrice The gasprice of the transaction. This is provided so that the function call can continue 100 | // /// to be pure rather than view. 101 | // /// @param shouldMaximallyFillOrders A value that indicates whether or not this calculation should use 102 | // /// the maximal fill order matching strategy. 103 | // /// @param matchedFillResults Amounts to fill and fees to pay by maker and taker of matched orders. 104 | // function calculateMatchedFillResults( 105 | // LibOrder.Order memory leftOrder, 106 | // LibOrder.Order memory rightOrder, 107 | // uint256 leftOrderTakerAssetFilledAmount, 108 | // uint256 rightOrderTakerAssetFilledAmount, 109 | // uint256 protocolFeeMultiplier, 110 | // uint256 gasPrice, 111 | // bool shouldMaximallyFillOrders 112 | // ) 113 | // internal 114 | // pure 115 | // returns (MatchedFillResults memory matchedFillResults) 116 | // { 117 | // // Derive maker asset amounts for left & right orders, given store taker assert amounts 118 | // uint256 leftTakerAssetAmountRemaining = leftOrder.takerAssetAmount.safeSub(leftOrderTakerAssetFilledAmount); 119 | // uint256 leftMakerAssetAmountRemaining = LibMath.safeGetPartialAmountFloor( 120 | // leftOrder.makerAssetAmount, 121 | // leftOrder.takerAssetAmount, 122 | // leftTakerAssetAmountRemaining 123 | // ); 124 | // uint256 rightTakerAssetAmountRemaining = rightOrder.takerAssetAmount.safeSub(rightOrderTakerAssetFilledAmount); 125 | // uint256 rightMakerAssetAmountRemaining = LibMath.safeGetPartialAmountFloor( 126 | // rightOrder.makerAssetAmount, 127 | // rightOrder.takerAssetAmount, 128 | // rightTakerAssetAmountRemaining 129 | // ); 130 | 131 | // // Maximally fill the orders and pay out profits to the matcher in one or both of the maker assets. 132 | // if (shouldMaximallyFillOrders) { 133 | // matchedFillResults = _calculateMatchedFillResultsWithMaximalFill( 134 | // leftOrder, 135 | // rightOrder, 136 | // leftMakerAssetAmountRemaining, 137 | // leftTakerAssetAmountRemaining, 138 | // rightMakerAssetAmountRemaining, 139 | // rightTakerAssetAmountRemaining 140 | // ); 141 | // } else { 142 | // matchedFillResults = _calculateMatchedFillResults( 143 | // leftOrder, 144 | // rightOrder, 145 | // leftMakerAssetAmountRemaining, 146 | // leftTakerAssetAmountRemaining, 147 | // rightMakerAssetAmountRemaining, 148 | // rightTakerAssetAmountRemaining 149 | // ); 150 | // } 151 | 152 | // // Compute fees for left order 153 | // matchedFillResults.left.makerFeePaid = LibMath.safeGetPartialAmountFloor( 154 | // matchedFillResults.left.makerAssetFilledAmount, 155 | // leftOrder.makerAssetAmount, 156 | // leftOrder.makerFee 157 | // ); 158 | // matchedFillResults.left.takerFeePaid = LibMath.safeGetPartialAmountFloor( 159 | // matchedFillResults.left.takerAssetFilledAmount, 160 | // leftOrder.takerAssetAmount, 161 | // leftOrder.takerFee 162 | // ); 163 | 164 | // // Compute fees for right order 165 | // matchedFillResults.right.makerFeePaid = LibMath.safeGetPartialAmountFloor( 166 | // matchedFillResults.right.makerAssetFilledAmount, 167 | // rightOrder.makerAssetAmount, 168 | // rightOrder.makerFee 169 | // ); 170 | // matchedFillResults.right.takerFeePaid = LibMath.safeGetPartialAmountFloor( 171 | // matchedFillResults.right.takerAssetFilledAmount, 172 | // rightOrder.takerAssetAmount, 173 | // rightOrder.takerFee 174 | // ); 175 | 176 | // // Compute the protocol fee that should be paid for a single fill. In this 177 | // // case this should be made the protocol fee for both the left and right orders. 178 | // uint256 protocolFee = gasPrice.safeMul(protocolFeeMultiplier); 179 | // matchedFillResults.left.protocolFeePaid = protocolFee; 180 | // matchedFillResults.right.protocolFeePaid = protocolFee; 181 | 182 | // // Return fill results 183 | // return matchedFillResults; 184 | // } 185 | 186 | // /// @dev Adds properties of both FillResults instances. 187 | // /// @param fillResults1 The first FillResults. 188 | // /// @param fillResults2 The second FillResults. 189 | // /// @return The sum of both fill results. 190 | // function addFillResults( 191 | // FillResults memory fillResults1, 192 | // FillResults memory fillResults2 193 | // ) 194 | // internal 195 | // pure 196 | // returns (FillResults memory totalFillResults) 197 | // { 198 | // totalFillResults.makerAssetFilledAmount = fillResults1.makerAssetFilledAmount.safeAdd(fillResults2.makerAssetFilledAmount); 199 | // totalFillResults.takerAssetFilledAmount = fillResults1.takerAssetFilledAmount.safeAdd(fillResults2.takerAssetFilledAmount); 200 | // totalFillResults.makerFeePaid = fillResults1.makerFeePaid.safeAdd(fillResults2.makerFeePaid); 201 | // totalFillResults.takerFeePaid = fillResults1.takerFeePaid.safeAdd(fillResults2.takerFeePaid); 202 | // totalFillResults.protocolFeePaid = fillResults1.protocolFeePaid.safeAdd(fillResults2.protocolFeePaid); 203 | 204 | // return totalFillResults; 205 | // } 206 | 207 | // /// @dev Calculates part of the matched fill results for a given situation using the fill strategy that only 208 | // /// awards profit denominated in the left maker asset. 209 | // /// @param leftOrder The left order in the order matching situation. 210 | // /// @param rightOrder The right order in the order matching situation. 211 | // /// @param leftMakerAssetAmountRemaining The amount of the left order maker asset that can still be filled. 212 | // /// @param leftTakerAssetAmountRemaining The amount of the left order taker asset that can still be filled. 213 | // /// @param rightMakerAssetAmountRemaining The amount of the right order maker asset that can still be filled. 214 | // /// @param rightTakerAssetAmountRemaining The amount of the right order taker asset that can still be filled. 215 | // /// @return MatchFillResults struct that does not include fees paid. 216 | // function _calculateMatchedFillResults( 217 | // LibOrder.Order memory leftOrder, 218 | // LibOrder.Order memory rightOrder, 219 | // uint256 leftMakerAssetAmountRemaining, 220 | // uint256 leftTakerAssetAmountRemaining, 221 | // uint256 rightMakerAssetAmountRemaining, 222 | // uint256 rightTakerAssetAmountRemaining 223 | // ) 224 | // private 225 | // pure 226 | // returns (MatchedFillResults memory matchedFillResults) 227 | // { 228 | // // Calculate fill results for maker and taker assets: at least one order will be fully filled. 229 | // // The maximum amount the left maker can buy is `leftTakerAssetAmountRemaining` 230 | // // The maximum amount the right maker can sell is `rightMakerAssetAmountRemaining` 231 | // // We have two distinct cases for calculating the fill results: 232 | // // Case 1. 233 | // // If the left maker can buy more than the right maker can sell, then only the right order is fully filled. 234 | // // If the left maker can buy exactly what the right maker can sell, then both orders are fully filled. 235 | // // Case 2. 236 | // // If the left maker cannot buy more than the right maker can sell, then only the left order is fully filled. 237 | // // Case 3. 238 | // // If the left maker can buy exactly as much as the right maker can sell, then both orders are fully filled. 239 | // if (leftTakerAssetAmountRemaining > rightMakerAssetAmountRemaining) { 240 | // // Case 1: Right order is fully filled 241 | // matchedFillResults = _calculateCompleteRightFill( 242 | // leftOrder, 243 | // rightMakerAssetAmountRemaining, 244 | // rightTakerAssetAmountRemaining 245 | // ); 246 | // } else if (leftTakerAssetAmountRemaining < rightMakerAssetAmountRemaining) { 247 | // // Case 2: Left order is fully filled 248 | // matchedFillResults.left.makerAssetFilledAmount = leftMakerAssetAmountRemaining; 249 | // matchedFillResults.left.takerAssetFilledAmount = leftTakerAssetAmountRemaining; 250 | // matchedFillResults.right.makerAssetFilledAmount = leftTakerAssetAmountRemaining; 251 | // // Round up to ensure the maker's exchange rate does not exceed the price specified by the order. 252 | // // We favor the maker when the exchange rate must be rounded. 253 | // matchedFillResults.right.takerAssetFilledAmount = LibMath.safeGetPartialAmountCeil( 254 | // rightOrder.takerAssetAmount, 255 | // rightOrder.makerAssetAmount, 256 | // leftTakerAssetAmountRemaining // matchedFillResults.right.makerAssetFilledAmount 257 | // ); 258 | // } else { 259 | // // leftTakerAssetAmountRemaining == rightMakerAssetAmountRemaining 260 | // // Case 3: Both orders are fully filled. Technically, this could be captured by the above cases, but 261 | // // this calculation will be more precise since it does not include rounding. 262 | // matchedFillResults = _calculateCompleteFillBoth( 263 | // leftMakerAssetAmountRemaining, 264 | // leftTakerAssetAmountRemaining, 265 | // rightMakerAssetAmountRemaining, 266 | // rightTakerAssetAmountRemaining 267 | // ); 268 | // } 269 | 270 | // // Calculate amount given to taker 271 | // matchedFillResults.profitInLeftMakerAsset = matchedFillResults.left.makerAssetFilledAmount.safeSub( 272 | // matchedFillResults.right.takerAssetFilledAmount 273 | // ); 274 | 275 | // return matchedFillResults; 276 | // } 277 | 278 | // /// @dev Calculates part of the matched fill results for a given situation using the maximal fill order matching 279 | // /// strategy. 280 | // /// @param leftOrder The left order in the order matching situation. 281 | // /// @param rightOrder The right order in the order matching situation. 282 | // /// @param leftMakerAssetAmountRemaining The amount of the left order maker asset that can still be filled. 283 | // /// @param leftTakerAssetAmountRemaining The amount of the left order taker asset that can still be filled. 284 | // /// @param rightMakerAssetAmountRemaining The amount of the right order maker asset that can still be filled. 285 | // /// @param rightTakerAssetAmountRemaining The amount of the right order taker asset that can still be filled. 286 | // /// @return MatchFillResults struct that does not include fees paid. 287 | // function _calculateMatchedFillResultsWithMaximalFill( 288 | // LibOrder.Order memory leftOrder, 289 | // LibOrder.Order memory rightOrder, 290 | // uint256 leftMakerAssetAmountRemaining, 291 | // uint256 leftTakerAssetAmountRemaining, 292 | // uint256 rightMakerAssetAmountRemaining, 293 | // uint256 rightTakerAssetAmountRemaining 294 | // ) 295 | // private 296 | // pure 297 | // returns (MatchedFillResults memory matchedFillResults) 298 | // { 299 | // // If a maker asset is greater than the opposite taker asset, than there will be a spread denominated in that maker asset. 300 | // bool doesLeftMakerAssetProfitExist = leftMakerAssetAmountRemaining > rightTakerAssetAmountRemaining; 301 | // bool doesRightMakerAssetProfitExist = rightMakerAssetAmountRemaining > leftTakerAssetAmountRemaining; 302 | 303 | // // Calculate the maximum fill results for the maker and taker assets. At least one of the orders will be fully filled. 304 | // // 305 | // // The maximum that the left maker can possibly buy is the amount that the right order can sell. 306 | // // The maximum that the right maker can possibly buy is the amount that the left order can sell. 307 | // // 308 | // // If the left order is fully filled, profit will be paid out in the left maker asset. If the right order is fully filled, 309 | // // the profit will be out in the right maker asset. 310 | // // 311 | // // There are three cases to consider: 312 | // // Case 1. 313 | // // If the left maker can buy more than the right maker can sell, then only the right order is fully filled. 314 | // // Case 2. 315 | // // If the right maker can buy more than the left maker can sell, then only the right order is fully filled. 316 | // // Case 3. 317 | // // If the right maker can sell the max of what the left maker can buy and the left maker can sell the max of 318 | // // what the right maker can buy, then both orders are fully filled. 319 | // if (leftTakerAssetAmountRemaining > rightMakerAssetAmountRemaining) { 320 | // // Case 1: Right order is fully filled with the profit paid in the left makerAsset 321 | // matchedFillResults = _calculateCompleteRightFill( 322 | // leftOrder, 323 | // rightMakerAssetAmountRemaining, 324 | // rightTakerAssetAmountRemaining 325 | // ); 326 | // } else if (rightTakerAssetAmountRemaining > leftMakerAssetAmountRemaining) { 327 | // // Case 2: Left order is fully filled with the profit paid in the right makerAsset. 328 | // matchedFillResults.left.makerAssetFilledAmount = leftMakerAssetAmountRemaining; 329 | // matchedFillResults.left.takerAssetFilledAmount = leftTakerAssetAmountRemaining; 330 | // // Round down to ensure the right maker's exchange rate does not exceed the price specified by the order. 331 | // // We favor the right maker when the exchange rate must be rounded and the profit is being paid in the 332 | // // right maker asset. 333 | // matchedFillResults.right.makerAssetFilledAmount = LibMath.safeGetPartialAmountFloor( 334 | // rightOrder.makerAssetAmount, 335 | // rightOrder.takerAssetAmount, 336 | // leftMakerAssetAmountRemaining 337 | // ); 338 | // matchedFillResults.right.takerAssetFilledAmount = leftMakerAssetAmountRemaining; 339 | // } else { 340 | // // Case 3: The right and left orders are fully filled 341 | // matchedFillResults = _calculateCompleteFillBoth( 342 | // leftMakerAssetAmountRemaining, 343 | // leftTakerAssetAmountRemaining, 344 | // rightMakerAssetAmountRemaining, 345 | // rightTakerAssetAmountRemaining 346 | // ); 347 | // } 348 | 349 | // // Calculate amount given to taker in the left order's maker asset if the left spread will be part of the profit. 350 | // if (doesLeftMakerAssetProfitExist) { 351 | // matchedFillResults.profitInLeftMakerAsset = matchedFillResults.left.makerAssetFilledAmount.safeSub( 352 | // matchedFillResults.right.takerAssetFilledAmount 353 | // ); 354 | // } 355 | 356 | // // Calculate amount given to taker in the right order's maker asset if the right spread will be part of the profit. 357 | // if (doesRightMakerAssetProfitExist) { 358 | // matchedFillResults.profitInRightMakerAsset = matchedFillResults.right.makerAssetFilledAmount.safeSub( 359 | // matchedFillResults.left.takerAssetFilledAmount 360 | // ); 361 | // } 362 | 363 | // return matchedFillResults; 364 | // } 365 | 366 | // /// @dev Calculates the fill results for the maker and taker in the order matching and writes the results 367 | // /// to the fillResults that are being collected on the order. Both orders will be fully filled in this 368 | // /// case. 369 | // /// @param leftMakerAssetAmountRemaining The amount of the left maker asset that is remaining to be filled. 370 | // /// @param leftTakerAssetAmountRemaining The amount of the left taker asset that is remaining to be filled. 371 | // /// @param rightMakerAssetAmountRemaining The amount of the right maker asset that is remaining to be filled. 372 | // /// @param rightTakerAssetAmountRemaining The amount of the right taker asset that is remaining to be filled. 373 | // /// @return MatchFillResults struct that does not include fees paid or spreads taken. 374 | // function _calculateCompleteFillBoth( 375 | // uint256 leftMakerAssetAmountRemaining, 376 | // uint256 leftTakerAssetAmountRemaining, 377 | // uint256 rightMakerAssetAmountRemaining, 378 | // uint256 rightTakerAssetAmountRemaining 379 | // ) 380 | // private 381 | // pure 382 | // returns (MatchedFillResults memory matchedFillResults) 383 | // { 384 | // // Calculate the fully filled results for both orders. 385 | // matchedFillResults.left.makerAssetFilledAmount = leftMakerAssetAmountRemaining; 386 | // matchedFillResults.left.takerAssetFilledAmount = leftTakerAssetAmountRemaining; 387 | // matchedFillResults.right.makerAssetFilledAmount = rightMakerAssetAmountRemaining; 388 | // matchedFillResults.right.takerAssetFilledAmount = rightTakerAssetAmountRemaining; 389 | 390 | // return matchedFillResults; 391 | // } 392 | 393 | // /// @dev Calculates the fill results for the maker and taker in the order matching and writes the results 394 | // /// to the fillResults that are being collected on the order. 395 | // /// @param leftOrder The left order that is being maximally filled. All of the information about fill amounts 396 | // /// can be derived from this order and the right asset remaining fields. 397 | // /// @param rightMakerAssetAmountRemaining The amount of the right maker asset that is remaining to be filled. 398 | // /// @param rightTakerAssetAmountRemaining The amount of the right taker asset that is remaining to be filled. 399 | // /// @return MatchFillResults struct that does not include fees paid or spreads taken. 400 | // function _calculateCompleteRightFill( 401 | // LibOrder.Order memory leftOrder, 402 | // uint256 rightMakerAssetAmountRemaining, 403 | // uint256 rightTakerAssetAmountRemaining 404 | // ) 405 | // private 406 | // pure 407 | // returns (MatchedFillResults memory matchedFillResults) 408 | // { 409 | // matchedFillResults.right.makerAssetFilledAmount = rightMakerAssetAmountRemaining; 410 | // matchedFillResults.right.takerAssetFilledAmount = rightTakerAssetAmountRemaining; 411 | // matchedFillResults.left.takerAssetFilledAmount = rightMakerAssetAmountRemaining; 412 | // // Round down to ensure the left maker's exchange rate does not exceed the price specified by the order. 413 | // // We favor the left maker when the exchange rate must be rounded and the profit is being paid in the 414 | // // left maker asset. 415 | // matchedFillResults.left.makerAssetFilledAmount = LibMath.safeGetPartialAmountFloor( 416 | // leftOrder.makerAssetAmount, 417 | // leftOrder.takerAssetAmount, 418 | // rightMakerAssetAmountRemaining 419 | // ); 420 | 421 | // return matchedFillResults; 422 | // } 423 | } 424 | -------------------------------------------------------------------------------- /contracts/lib/LibOrder.sol: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2019 ZeroEx Intl. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | // Extracted from from https://github.com/0xProject/0x-monorepo/blob/development/contracts/exchange-libs/contracts/src/LibOrder.sol 20 | 21 | pragma solidity 0.8.0; 22 | 23 | library LibOrder { 24 | 25 | using LibOrder for Order; 26 | 27 | // A valid order remains fillable until it is expired, fully filled, or cancelled. 28 | // An order's status is unaffected by external factors, like account balances. 29 | enum OrderStatus { 30 | INVALID, // Default value 31 | INVALID_MAKER_ASSET_AMOUNT, // Order does not have a valid maker asset amount 32 | INVALID_TAKER_ASSET_AMOUNT, // Order does not have a valid taker asset amount 33 | FILLABLE, // Order is fillable 34 | EXPIRED, // Order has already expired 35 | FULLY_FILLED, // Order is fully filled 36 | CANCELLED // Order has been cancelled 37 | } 38 | 39 | // solhint-disable max-line-length 40 | /// @dev Canonical order structure. 41 | struct Order { 42 | address makerAddress; // Address that created the order. 43 | address takerAddress; // Address that is allowed to fill the order. If set to 0, any address is allowed to fill the order. 44 | address feeRecipientAddress; // Address that will recieve fees when order is filled. 45 | address senderAddress; // 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. 46 | uint256 makerAssetAmount; // Amount of makerAsset being offered by maker. Must be greater than 0. 47 | uint256 takerAssetAmount; // Amount of takerAsset being bid on by maker. Must be greater than 0. 48 | uint256 makerFee; // Fee paid to feeRecipient by maker when order is filled. 49 | uint256 takerFee; // Fee paid to feeRecipient by taker when order is filled. 50 | uint256 expirationTimeSeconds; // Timestamp in seconds at which order expires. 51 | uint256 salt; // Arbitrary number to facilitate uniqueness of the order's hash. 52 | bytes makerAssetData; // Encoded data that can be decoded by a specified proxy contract when transferring makerAsset. The leading bytes4 references the id of the asset proxy. 53 | bytes takerAssetData; // Encoded data that can be decoded by a specified proxy contract when transferring takerAsset. The leading bytes4 references the id of the asset proxy. 54 | bytes makerFeeAssetData; // Encoded data that can be decoded by a specified proxy contract when transferring makerFeeAsset. The leading bytes4 references the id of the asset proxy. 55 | bytes takerFeeAssetData; // Encoded data that can be decoded by a specified proxy contract when transferring takerFeeAsset. The leading bytes4 references the id of the asset proxy. 56 | } 57 | // solhint-enable max-line-length 58 | 59 | } 60 | -------------------------------------------------------------------------------- /contracts/lib/ReentrancyGuard.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.0; 2 | 3 | contract ReentrancyGuard { 4 | bool reentrancyLock = false; 5 | 6 | modifier reentrancyGuard { 7 | require(!reentrancyLock, "Reentrancy detected"); 8 | reentrancyLock = true; 9 | _; 10 | reentrancyLock = false; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/test/ExchangeMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.0; 2 | pragma abicoder v2; 3 | 4 | import "../lib/ReentrancyGuard.sol"; 5 | import "../lib/LibOrder.sol"; 6 | import "../lib/LibFillResults.sol"; 7 | import "../lib/ArrayUtils.sol"; 8 | 9 | contract ExchangeMock is ReentrancyGuard { 10 | bytes4 _erc20 = hex"f47261b0"; 11 | bytes4 _erc721 = hex"02571792"; 12 | bytes4 _erc1155 = hex"a7cb5fb7"; 13 | 14 | function getAssetProxy(bytes4 assetProxyId) 15 | external 16 | view 17 | returns (address assetProxy) 18 | { 19 | return address(this); 20 | } 21 | 22 | modifier refundRemainingBalance() 23 | { 24 | _; 25 | uint256 balance = address(this).balance; 26 | if (balance > 0) { 27 | payable(msg.sender).transfer(balance); 28 | } 29 | } 30 | 31 | function matchOrders( 32 | LibOrder.Order memory leftOrder, 33 | LibOrder.Order memory rightOrder, 34 | bytes memory leftSignature, 35 | bytes memory rightSignature 36 | ) 37 | external 38 | payable 39 | reentrancyGuard 40 | refundRemainingBalance 41 | returns (LibFillResults.MatchedFillResults memory matchedFillResults) 42 | { 43 | transferTokens(leftOrder.makerAssetData,leftOrder.makerAddress,rightOrder.makerAddress,leftOrder.makerAssetAmount); 44 | transferTokens(rightOrder.makerAssetData,rightOrder.makerAddress,leftOrder.makerAddress,rightOrder.makerAssetAmount); 45 | if (leftOrder.makerFee > 0) 46 | transferTokens(leftOrder.makerFeeAssetData,leftOrder.makerAddress,leftOrder.feeRecipientAddress,leftOrder.makerFee); 47 | if (rightOrder.makerFee > 0) 48 | transferTokens(rightOrder.makerFeeAssetData,rightOrder.makerAddress,rightOrder.feeRecipientAddress,rightOrder.makerFee); 49 | //TODO- fill MatchedFillResults 50 | return matchedFillResults; 51 | } 52 | 53 | function extractBytes4(bytes memory data) 54 | internal 55 | pure 56 | returns (bytes4) 57 | { 58 | require(data.length >= 4,"Data too short"); 59 | return bytes4(data[0]) | (bytes4(data[1]) >> 8) | (bytes4(data[2]) >> 16) | (bytes4(data[3]) >> 24); 60 | } 61 | 62 | function transferTokens(bytes memory assetData,address from, address to, uint256 amount) 63 | internal 64 | { 65 | bytes4 proxyId = extractBytes4(assetData); 66 | if (proxyId == _erc20) 67 | transferERC20(assetData,from,to,amount); 68 | else if (proxyId == _erc721) 69 | transferERC721(assetData,from,to); 70 | else if (proxyId == _erc1155) 71 | transferERC1155(assetData,from,to,amount); 72 | else 73 | revert("Unknown proxyID standard"); 74 | } 75 | 76 | function transferERC20(bytes memory assetData,address from, address to, uint256 amount) 77 | internal 78 | { 79 | (address contractAddress) = abi.decode(ArrayUtils.arrayDrop(assetData,4), (address)); 80 | (bool success,) = contractAddress.call(abi.encodeWithSignature("transferFrom(address,address,uint256)",from,to,amount)); 81 | require(success,"ERC20 transfer failed"); 82 | } 83 | 84 | function transferERC721(bytes memory assetData,address from, address to) 85 | internal 86 | { 87 | (address contractAddress, uint256 tokenId) = abi.decode(ArrayUtils.arrayDrop(assetData,4), (address,uint256)); 88 | (bool success,) = contractAddress.call(abi.encodeWithSignature("transferFrom(address,address,uint256)",from,to,tokenId)); 89 | require(success,"ERC721 transfer failed"); 90 | } 91 | 92 | function transferERC1155(bytes memory assetData,address from, address to, uint256 amount) 93 | internal 94 | { 95 | (address contractAddress, uint256[] memory tokenIds, uint256[] memory amounts, bytes memory extra) = abi.decode(ArrayUtils.arrayDrop(assetData,4), (address,uint256[],uint256[],bytes)); 96 | require(tokenIds.length > 0,"No token IDs"); 97 | (bool success,) = contractAddress.call(abi.encodeWithSignature("safeTransferFrom(address,address,uint256,uint256,bytes)",from,to,tokenIds[0],amount,extra)); 98 | require(success,"ERC1155 transfer failed"); 99 | } 100 | } -------------------------------------------------------------------------------- /contracts/test/TestERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 4 | 5 | contract TestERC20 is ERC20("Test20", "TST20") { 6 | constructor () public { 7 | } 8 | 9 | function mint(address to, uint256 value) public returns (bool) { 10 | _mint(to, value); 11 | return true; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/test/TestERC721.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 4 | 5 | contract TestERC721 is ERC721("Test721", "TST721") { 6 | constructor () public { 7 | } 8 | 9 | function mint(address to, uint256 tokenId) public returns (bool) { 10 | _mint(to, tokenId); 11 | return true; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | const ExchangeMock = artifacts.require("ExchangeMock"); 3 | const ZeroExFeeWrapper = artifacts.require("ZeroExFeeWrapper"); 4 | 5 | const zeroExExchangeAddresses = { 6 | ethereum: "0x61935cbdd02287b511119ddb11aeb42f1593b7ef", 7 | rinkeby: "0xf8becacec90bfc361c0a2c720839e08405a72f6d", 8 | matic: "0xfede379e48c873c75f3cc0c81f7c784ad730a8f7", 9 | mumbai: "0x533dc89624dcc012c7323b41f286bd2df478800b", 10 | klaytn: "0xaadefaa2b05fc765902a92121abb6ad9e8233c2c", 11 | baobab: "0x3887cf551a1Bd89f04775e224e6954083A23380D" 12 | } 13 | 14 | module.exports = async (deployer,network) => { 15 | await deployer.deploy(Migrations); 16 | const exchangeAddress = network === 'development' 17 | ? (await deployer.deploy(ExchangeMock) && ExchangeMock.address) 18 | : zeroExExchangeAddresses[network]; 19 | if (!exchangeAddress) 20 | throw new Error(`No 0x exchange address for network ${network}`); 21 | await deployer.deploy(ZeroExFeeWrapper,exchangeAddress); 22 | }; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "0x-fee-wrapper-contract", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "truffle test", 8 | "test-rpc": "ganache-cli --networkId 50 --port 8545" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/MarvinJanssen/0x-fee-wrapper-contract.git" 13 | }, 14 | "keywords": [], 15 | "author": "Marvin Janssen", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/MarvinJanssen/0x-fee-wrapper-contract/issues" 19 | }, 20 | "homepage": "https://github.com/MarvinJanssen/0x-fee-wrapper-contract#readme", 21 | "devDependencies": { 22 | "@openzeppelin/contracts": "^4.1.0", 23 | "ganache-cli": "^6.12.2", 24 | "truffle": "^5.3.7" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/0-exchange-mock.js: -------------------------------------------------------------------------------- 1 | const ExchangeMock = artifacts.require('ExchangeMock'); 2 | const TestERC20 = artifacts.require('TestERC20'); 3 | const TestERC721 = artifacts.require('TestERC721'); 4 | 5 | const {makeOrder,encodeAssetData,proxyIds} = require('./util'); 6 | 7 | contract.skip('ExchangeMock',accounts => { 8 | 9 | const deploy = async (list) => { 10 | return await Promise.all(list.map(list => list.new())); 11 | }; 12 | 13 | it('exchanges tokens',async () => { 14 | const [exchange,erc20,erc721] = await deploy([ExchangeMock,TestERC20,TestERC721]); 15 | const [deployer,accountA,accountB] = accounts; 16 | const [erc20Proxy,erc721Proxy] = await Promise.all([exchange.getAssetProxy(proxyIds.erc20),exchange.getAssetProxy(proxyIds.erc721)]); 17 | const tokenId = 5; 18 | const paymentAssetAmount = 2000; 19 | await Promise.all([erc721.mint(accountA,tokenId),erc20.mint(accountB,paymentAssetAmount)]); 20 | await Promise.all([erc721.setApprovalForAll(erc721Proxy,true,{from:accountA}),erc20.approve(erc20Proxy,paymentAssetAmount,{from:accountB})]); 21 | 22 | const leftAssetData = encodeAssetData('erc721',{contractAddress: erc721.address,tokenId}); 23 | const rightAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 24 | 25 | const leftOrder = makeOrder({ 26 | senderAddress: deployer, 27 | makerAddress: accountA, 28 | makerAssetData: leftAssetData, 29 | makerAssetAmount: 1, 30 | takerAssetData: rightAssetData, 31 | takerAssetAmount: paymentAssetAmount, 32 | }); 33 | 34 | const rightOrder = makeOrder({ 35 | senderAddress: deployer, 36 | makerAddress: accountB, 37 | makerAssetData: rightAssetData, 38 | makerAssetAmount: paymentAssetAmount, 39 | takerAssetData: leftAssetData, 40 | takerAssetAmount: 1, 41 | }); 42 | 43 | await exchange.matchOrders(leftOrder,rightOrder,"0x","0x",{from:deployer}); 44 | 45 | assert.equal((await erc20.balanceOf(accountA)).toNumber(),paymentAssetAmount); 46 | assert.equal(await erc721.ownerOf(tokenId),accountB); 47 | }); 48 | 49 | it('transfers fees',async () => { 50 | const [exchange,erc20,erc721] = await deploy([ExchangeMock,TestERC20,TestERC721]); 51 | const [deployer,accountA,accountB] = accounts; 52 | const [erc20Proxy,erc721Proxy] = await Promise.all([exchange.getAssetProxy(proxyIds.erc20),exchange.getAssetProxy(proxyIds.erc721)]); 53 | const tokenId = 5; 54 | const paymentAssetAmount = 2000; 55 | const paymentAssetFee = 50; 56 | await Promise.all([erc721.mint(accountA,tokenId),erc20.mint(accountB,paymentAssetAmount + paymentAssetFee)]); 57 | await Promise.all([erc721.setApprovalForAll(erc721Proxy,true,{from:accountA}),erc20.approve(erc20Proxy,paymentAssetAmount + paymentAssetFee,{from:accountB})]); 58 | 59 | const leftAssetData = encodeAssetData('erc721',{contractAddress: erc721.address,tokenId}); 60 | const rightAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 61 | 62 | const rightFeeAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 63 | 64 | const leftOrder = makeOrder({ 65 | senderAddress: deployer, 66 | makerAddress: accountA, 67 | makerAssetData: leftAssetData, 68 | makerAssetAmount: 1, 69 | takerAssetData: rightAssetData, 70 | takerAssetAmount: paymentAssetAmount, 71 | }); 72 | 73 | const rightOrder = makeOrder({ 74 | senderAddress: deployer, 75 | makerAddress: accountB, 76 | makerAssetData: rightAssetData, 77 | makerAssetAmount: paymentAssetAmount, 78 | takerAssetData: leftAssetData, 79 | takerAssetAmount: 1, 80 | feeRecipientAddress: deployer, 81 | makerFeeAssetData: rightFeeAssetData, 82 | makerFee: paymentAssetFee 83 | }); 84 | 85 | await exchange.matchOrders(leftOrder,rightOrder,"0x","0x",{from:deployer}); 86 | 87 | assert.equal((await erc20.balanceOf(deployer)).toNumber(),paymentAssetFee); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/1-fee-exchange-wrapper.js: -------------------------------------------------------------------------------- 1 | const ExchangeMock = artifacts.require('ExchangeMock'); 2 | const TestERC20 = artifacts.require('TestERC20'); 3 | const TestERC721 = artifacts.require('TestERC721'); 4 | const ZeroExFeeWrapper = artifacts.require('ZeroExFeeWrapper'); 5 | 6 | const BN = require('bn.js'); 7 | const {NULL_ADDRESS,assertIsRejected,makeOrder,encodeAssetData,proxyIds} = require('./util'); 8 | 9 | contract('ZeroExFeeWrapper',accounts => { 10 | 11 | const deploy = async (list) => { 12 | return await Promise.all(list.map(list => list.new())); 13 | }; 14 | 15 | const prepareBasicTest = async (options) => { 16 | const [exchange,erc20,erc721] = await deploy([ExchangeMock,TestERC20,TestERC721]); 17 | const wrapper = await ZeroExFeeWrapper.new(exchange.address); 18 | const {accountA,accountB,tokenId,paymentAssetAmount,paymentAssetFee = 0} = options; 19 | const [erc20Proxy,erc721Proxy] = await Promise.all([exchange.getAssetProxy(proxyIds.erc20),exchange.getAssetProxy(proxyIds.erc721)]); 20 | await Promise.all([erc721.mint(accountA,tokenId),erc20.mint(accountB,paymentAssetAmount + paymentAssetFee)]); 21 | await Promise.all([erc721.setApprovalForAll(erc721Proxy,true,{from:accountA}),erc20.approve(erc20Proxy,paymentAssetAmount + paymentAssetFee,{from:accountB})]); 22 | return {exchange,wrapper,erc20,erc721}; 23 | }; 24 | 25 | it('allows owners to add new owners',async () => { 26 | const [deployer,accountA] = accounts; 27 | const wrapper = await ZeroExFeeWrapper.new(NULL_ADDRESS); 28 | await wrapper.setOwner(accountA,true,{from:deployer}); 29 | assert.isTrue(await wrapper.isOwner(accountA)); 30 | }); 31 | 32 | it('does not allow non-owners to add new owners',async () => { 33 | const [,accountA,accountB] = accounts; 34 | const wrapper = await ZeroExFeeWrapper.new(NULL_ADDRESS); 35 | return assertIsRejected( 36 | wrapper.setOwner(accountA,true,{from:accountB}), 37 | /Owner only/ 38 | ); 39 | }); 40 | 41 | it('allows owners to change the exchange address',async () => { 42 | const [deployer] = accounts; 43 | const wrapper = await ZeroExFeeWrapper.new(NULL_ADDRESS); 44 | const bogusAddress = "0x0000000000000000000000000000000000000101"; 45 | await wrapper.setExchange(bogusAddress,{from:deployer}); 46 | assert.equal(await wrapper.getExchange(),bogusAddress); 47 | }); 48 | 49 | it('does not allow non-owners to change the exchange address',async () => { 50 | const [,accountA] = accounts; 51 | const wrapper = await ZeroExFeeWrapper.new(NULL_ADDRESS); 52 | const bogusAddress = "0x0000000000000000000000000000000000000101"; 53 | return assertIsRejected( 54 | wrapper.setExchange(bogusAddress,{from:accountA}), 55 | /Owner only/ 56 | ) 57 | }); 58 | 59 | it('does not allow non-owners to call matchOrders',async () => { 60 | const [,accountA,accountB] = accounts; 61 | const wrapper = await ZeroExFeeWrapper.new(NULL_ADDRESS); 62 | 63 | const order = makeOrder({ 64 | makerAddress: accountA, 65 | makerAssetData: "0x", 66 | makerAssetAmount: 1, 67 | takerAssetData: "0x", 68 | takerAssetAmount: 1, 69 | }); 70 | 71 | await assertIsRejected( 72 | wrapper.matchOrders(order,order,"0x","0x",[],NULL_ADDRESS,{from:accountB}), 73 | /Owner only/ 74 | ); 75 | }); 76 | 77 | it('exchanges tokens',async () => { 78 | const [deployer,accountA,accountB] = accounts; 79 | const tokenId = 5; 80 | const paymentAssetAmount = 2000; 81 | const {wrapper,erc20,erc721} = await prepareBasicTest({ 82 | accountA, 83 | accountB, 84 | tokenId, 85 | paymentAssetAmount 86 | }); 87 | 88 | const leftAssetData = encodeAssetData('erc721',{contractAddress: erc721.address,tokenId}); 89 | const rightAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 90 | 91 | const leftOrder = makeOrder({ 92 | makerAddress: accountA, 93 | makerAssetData: leftAssetData, 94 | makerAssetAmount: 1, 95 | takerAssetData: rightAssetData, 96 | takerAssetAmount: paymentAssetAmount, 97 | }); 98 | 99 | const rightOrder = makeOrder({ 100 | makerAddress: accountB, 101 | makerAssetData: rightAssetData, 102 | makerAssetAmount: paymentAssetAmount, 103 | takerAssetData: leftAssetData, 104 | takerAssetAmount: 1, 105 | }); 106 | 107 | await wrapper.matchOrders(leftOrder,rightOrder,"0x","0x",[],erc20.address,{from:deployer}); 108 | 109 | assert.equal((await erc20.balanceOf(accountA)).toNumber(),paymentAssetAmount); 110 | assert.equal(await erc721.ownerOf(tokenId),accountB); 111 | }); 112 | 113 | it('exchanges tokens with sender address set to the wrapper',async () => { 114 | const [deployer,accountA,accountB] = accounts; 115 | const tokenId = 5; 116 | const paymentAssetAmount = 2000; 117 | const {wrapper,erc20,erc721} = await prepareBasicTest({ 118 | accountA, 119 | accountB, 120 | tokenId, 121 | paymentAssetAmount 122 | }); 123 | 124 | const leftAssetData = encodeAssetData('erc721',{contractAddress: erc721.address,tokenId}); 125 | const rightAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 126 | 127 | const leftOrder = makeOrder({ 128 | makerAddress: accountA, 129 | makerAssetData: leftAssetData, 130 | makerAssetAmount: 1, 131 | takerAssetData: rightAssetData, 132 | takerAssetAmount: paymentAssetAmount, 133 | senderAddress: wrapper.address 134 | }); 135 | 136 | const rightOrder = makeOrder({ 137 | makerAddress: accountB, 138 | makerAssetData: rightAssetData, 139 | makerAssetAmount: paymentAssetAmount, 140 | takerAssetData: leftAssetData, 141 | takerAssetAmount: 1, 142 | senderAddress: wrapper.address 143 | }); 144 | 145 | await wrapper.matchOrders(leftOrder,rightOrder,"0x","0x",[],erc20.address,{from:deployer}); 146 | 147 | assert.equal((await erc20.balanceOf(accountA)).toNumber(),paymentAssetAmount); 148 | assert.equal(await erc721.ownerOf(tokenId),accountB); 149 | }); 150 | 151 | it('rejects orders that have the sender address set to anything other than the wrapper (if any)',async () => { 152 | const [deployer,accountA,accountB,accountC] = accounts; 153 | const wrapper = await ZeroExFeeWrapper.new(NULL_ADDRESS); 154 | 155 | const emptySenderOrder = makeOrder({ 156 | makerAddress: accountA, 157 | makerAssetData: "0x", 158 | makerAssetAmount: 1, 159 | takerAssetData: "0x", 160 | takerAssetAmount: 1, 161 | }); 162 | 163 | const senderOrder = makeOrder({ 164 | makerAddress: accountB, 165 | makerAssetData: "0x", 166 | makerAssetAmount: 1, 167 | takerAssetData: "0x", 168 | takerAssetAmount: 1, 169 | senderAddress: accountC 170 | }); 171 | 172 | await assertIsRejected( 173 | wrapper.matchOrders(senderOrder,emptySenderOrder,"0x","0x",[],NULL_ADDRESS,{from:deployer}), 174 | /leftOrder.senderAddress has to be 0x0 or the wrapper address/ 175 | ); 176 | await assertIsRejected( 177 | wrapper.matchOrders(emptySenderOrder,senderOrder,"0x","0x",[],NULL_ADDRESS,{from:deployer}), 178 | /rightOrder.senderAddress has to be 0x0 or the wrapper address/ 179 | ); 180 | await assertIsRejected( 181 | wrapper.matchOrders(senderOrder,senderOrder,"0x","0x",[],NULL_ADDRESS,{from:deployer}), 182 | /leftOrder.senderAddress has to be 0x0 or the wrapper address/ 183 | ); 184 | }); 185 | 186 | it('aborts if the order matching fails',async () => { 187 | const [deployer,accountA,accountB] = accounts; 188 | const tokenId = 5; 189 | const paymentAssetAmount = 2000; 190 | const {wrapper,erc20,erc721} = await prepareBasicTest({ 191 | accountA, 192 | accountB, 193 | tokenId, 194 | paymentAssetAmount 195 | }); 196 | 197 | const leftAssetData = encodeAssetData('erc721',{contractAddress: erc721.address,tokenId}); 198 | const rightAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 199 | 200 | const leftOrder = makeOrder({ 201 | makerAddress: accountA, 202 | makerAssetData: leftAssetData, 203 | makerAssetAmount: 1, 204 | takerAssetData: rightAssetData, 205 | takerAssetAmount: paymentAssetAmount, 206 | }); 207 | 208 | const rightOrder = makeOrder({ 209 | makerAddress: accountB, 210 | makerAssetData: rightAssetData, 211 | makerAssetAmount: paymentAssetAmount + 1000, 212 | takerAssetData: leftAssetData, 213 | takerAssetAmount: 1, 214 | }); 215 | 216 | await assertIsRejected( 217 | wrapper.matchOrders(leftOrder,rightOrder,"0x","0x",[],NULL_ADDRESS,{from:deployer}), 218 | /matchOrders failed/ 219 | ); 220 | }); 221 | 222 | it('transfers fees',async () => { 223 | const [deployer,accountA,accountB] = accounts; 224 | const tokenId = 5; 225 | const paymentAssetAmount = 2000; 226 | const paymentAssetFee = 50; 227 | const {wrapper,erc20,erc721} = await prepareBasicTest({ 228 | accountA, 229 | accountB, 230 | tokenId, 231 | paymentAssetAmount, 232 | paymentAssetFee 233 | }); 234 | 235 | const leftAssetData = encodeAssetData('erc721',{contractAddress: erc721.address,tokenId}); 236 | const rightAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 237 | const rightFeeAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 238 | 239 | const leftOrder = makeOrder({ 240 | makerAddress: accountA, 241 | makerAssetData: leftAssetData, 242 | makerAssetAmount: 1, 243 | takerAssetData: rightAssetData, 244 | takerAssetAmount: paymentAssetAmount, 245 | }); 246 | 247 | const rightOrder = makeOrder({ 248 | makerAddress: accountB, 249 | makerAssetData: rightAssetData, 250 | makerAssetAmount: paymentAssetAmount, 251 | takerAssetData: leftAssetData, 252 | takerAssetAmount: 1, 253 | feeRecipientAddress: deployer, 254 | makerFeeAssetData: rightFeeAssetData, 255 | makerFee: paymentAssetFee 256 | }); 257 | 258 | await wrapper.matchOrders(leftOrder,rightOrder,"0x","0x",[],erc20.address,{from:deployer}); 259 | 260 | assert.equal((await erc20.balanceOf(deployer)).toNumber(),paymentAssetFee); 261 | }); 262 | 263 | it('disburses fees',async () => { 264 | const [deployer,accountA,accountB,accountC,accountD] = accounts; 265 | const tokenId = 5; 266 | const paymentAssetAmount = 2000; 267 | const paymentAssetFee = 50; 268 | const feeAmountForC = 20; 269 | const feeAmountForD = 30; 270 | const {wrapper,erc20,erc721} = await prepareBasicTest({ 271 | accountA, 272 | accountB, 273 | tokenId, 274 | paymentAssetAmount, 275 | paymentAssetFee 276 | }); 277 | 278 | const leftAssetData = encodeAssetData('erc721',{contractAddress: erc721.address,tokenId}); 279 | const rightAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 280 | const rightFeeAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 281 | 282 | const leftOrder = makeOrder({ 283 | makerAddress: accountA, 284 | makerAssetData: leftAssetData, 285 | makerAssetAmount: 1, 286 | takerAssetData: rightAssetData, 287 | takerAssetAmount: paymentAssetAmount, 288 | }); 289 | 290 | const rightOrder = makeOrder({ 291 | makerAddress: accountB, 292 | makerAssetData: rightAssetData, 293 | makerAssetAmount: paymentAssetAmount, 294 | takerAssetData: leftAssetData, 295 | takerAssetAmount: 1, 296 | feeRecipientAddress: wrapper.address, 297 | makerFeeAssetData: rightFeeAssetData, 298 | makerFee: paymentAssetFee 299 | }); 300 | 301 | const feeData = [ 302 | {recipient: accountC, paymentTokenAmount: feeAmountForC}, 303 | {recipient: accountD, paymentTokenAmount: feeAmountForD} 304 | ]; 305 | 306 | await wrapper.matchOrders(leftOrder,rightOrder,"0x","0x",feeData,erc20.address,{from:deployer}); 307 | 308 | assert.equal((await erc20.balanceOf(accountC)).toNumber(),feeAmountForC); 309 | assert.equal((await erc20.balanceOf(accountD)).toNumber(),feeAmountForD); 310 | }); 311 | 312 | it('does not allow fees to remain',async () => { 313 | const [deployer,accountA,accountB,accountC,accountD] = accounts; 314 | const tokenId = 5; 315 | const paymentAssetAmount = 2000; 316 | const paymentAssetFee = 50; 317 | const feeAmountForC = 20; 318 | const feeAmountForD = 20; 319 | const {wrapper,erc20,erc721} = await prepareBasicTest({ 320 | accountA, 321 | accountB, 322 | tokenId, 323 | paymentAssetAmount, 324 | paymentAssetFee 325 | }); 326 | 327 | const leftAssetData = encodeAssetData('erc721',{contractAddress: erc721.address,tokenId}); 328 | const rightAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 329 | const rightFeeAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 330 | 331 | const leftOrder = makeOrder({ 332 | makerAddress: accountA, 333 | makerAssetData: leftAssetData, 334 | makerAssetAmount: 1, 335 | takerAssetData: rightAssetData, 336 | takerAssetAmount: paymentAssetAmount, 337 | }); 338 | 339 | const rightOrder = makeOrder({ 340 | makerAddress: accountB, 341 | makerAssetData: rightAssetData, 342 | makerAssetAmount: paymentAssetAmount, 343 | takerAssetData: leftAssetData, 344 | takerAssetAmount: 1, 345 | feeRecipientAddress: wrapper.address, 346 | makerFeeAssetData: rightFeeAssetData, 347 | makerFee: paymentAssetFee 348 | }); 349 | 350 | const feeData = [ 351 | {recipient: accountC, paymentTokenAmount: feeAmountForC}, 352 | {recipient: accountD, paymentTokenAmount: feeAmountForD} 353 | ]; 354 | 355 | return assertIsRejected( 356 | wrapper.matchOrders(leftOrder,rightOrder,"0x","0x",feeData,erc20.address,{from:deployer}), 357 | /Did not transfer the exact payment fee amount/ 358 | ); 359 | }); 360 | 361 | it('does not pay out more fees than it received',async () => { 362 | const [deployer,accountA,accountB,accountC,accountD] = accounts; 363 | const tokenId = 5; 364 | const paymentAssetAmount = 2000; 365 | const paymentAssetFee = 50; 366 | const feeAmountForC = 20; 367 | const feeAmountForD = 300; 368 | const {wrapper,erc20,erc721} = await prepareBasicTest({ 369 | accountA, 370 | accountB, 371 | tokenId, 372 | paymentAssetAmount, 373 | paymentAssetFee 374 | }); 375 | 376 | const surplusBalance = 5000; 377 | await erc20.mint(wrapper.address,surplusBalance); // Give the wrapper a surplus of the payment asset token. 378 | 379 | const leftAssetData = encodeAssetData('erc721',{contractAddress: erc721.address,tokenId}); 380 | const rightAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 381 | const rightFeeAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 382 | 383 | const leftOrder = makeOrder({ 384 | makerAddress: accountA, 385 | makerAssetData: leftAssetData, 386 | makerAssetAmount: 1, 387 | takerAssetData: rightAssetData, 388 | takerAssetAmount: paymentAssetAmount, 389 | }); 390 | 391 | const rightOrder = makeOrder({ 392 | makerAddress: accountB, 393 | makerAssetData: rightAssetData, 394 | makerAssetAmount: paymentAssetAmount, 395 | takerAssetData: leftAssetData, 396 | takerAssetAmount: 1, 397 | feeRecipientAddress: wrapper.address, 398 | makerFeeAssetData: rightFeeAssetData, 399 | makerFee: paymentAssetFee 400 | }); 401 | 402 | const feeData = [ 403 | {recipient: accountC, paymentTokenAmount: feeAmountForC}, 404 | {recipient: accountD, paymentTokenAmount: feeAmountForD} 405 | ]; 406 | 407 | await assertIsRejected( 408 | wrapper.matchOrders(leftOrder,rightOrder,"0x","0x",feeData,erc20.address,{from:deployer}), 409 | /Did not transfer the exact payment fee amount/ 410 | ); 411 | assert.equal(await erc20.balanceOf(wrapper.address),surplusBalance); 412 | }); 413 | 414 | it('requires the fee recipient address to be set to the wrapper (if any)',async () => { 415 | const [deployer,accountA,accountB,accountC,accountD,accountE] = accounts; 416 | const tokenId = 5; 417 | const paymentAssetAmount = 2000; 418 | const paymentAssetFee = 50; 419 | const feeAmountForC = 20; 420 | const feeAmountForD = 30; 421 | const {wrapper,erc20,erc721} = await prepareBasicTest({ 422 | accountA, 423 | accountB, 424 | tokenId, 425 | paymentAssetAmount, 426 | paymentAssetFee 427 | }); 428 | 429 | const leftAssetData = encodeAssetData('erc721',{contractAddress: erc721.address,tokenId}); 430 | const rightAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 431 | const rightFeeAssetData = encodeAssetData('erc20',{contractAddress: erc20.address}); 432 | 433 | const leftOrder = makeOrder({ 434 | makerAddress: accountA, 435 | makerAssetData: leftAssetData, 436 | makerAssetAmount: 1, 437 | takerAssetData: rightAssetData, 438 | takerAssetAmount: paymentAssetAmount, 439 | }); 440 | 441 | const rightOrder = makeOrder({ 442 | makerAddress: accountB, 443 | makerAssetData: rightAssetData, 444 | makerAssetAmount: paymentAssetAmount, 445 | takerAssetData: leftAssetData, 446 | takerAssetAmount: 1, 447 | feeRecipientAddress: accountE, 448 | makerFeeAssetData: rightFeeAssetData, 449 | makerFee: paymentAssetFee 450 | }); 451 | 452 | const feeData = [ 453 | {recipient: accountC, paymentTokenAmount: feeAmountForC}, 454 | {recipient: accountD, paymentTokenAmount: feeAmountForD} 455 | ]; 456 | 457 | await assertIsRejected( 458 | wrapper.matchOrders(leftOrder,rightOrder,"0x","0x",feeData,erc20.address,{from:deployer}), 459 | /Neither order has a fee recipient/ 460 | ); 461 | }); 462 | }); 463 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3'); 2 | const web3 = new Web3(); 3 | const BN = require('bn.js'); 4 | 5 | const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; 6 | 7 | const requiredOrderFields = ["makerAddress","makerAssetAmount","takerAssetAmount","makerAssetData","takerAssetData"]; 8 | 9 | const eth = web3.eth.extend({ 10 | methods: [{ 11 | name: 'signTypedData', 12 | call: 'eth_signTypedData', 13 | params: 2, 14 | inputFormatter: [web3.extend.formatters.inputAddressFormatter, null] 15 | }] 16 | }); 17 | 18 | async function signTypedData(data,account) { 19 | return await eth.signTypedData(account,data); 20 | } 21 | 22 | // Truffle does not expose chai so it is impossible to add chai-as-promised. 23 | // This is a simple replacement function. 24 | // https://github.com/trufflesuite/truffle/issues/2090 25 | function assertIsRejected(promise,error_match,message) { 26 | let passed = false; 27 | return promise 28 | .then(() => { 29 | passed = true; 30 | return assert.fail(); 31 | }) 32 | .catch(error => { 33 | if (passed) 34 | return assert.fail(message || 'Expected promise to be rejected') 35 | if (error_match) { 36 | if (typeof error_match === 'string') 37 | return assert.equal(error_match,error.message,message); 38 | if (error_match instanceof RegExp) 39 | return error.message.match(error_match) || assert.fail(error.message,error_match.toString(),`'${error.message}' does not match ${error_match.toString()}: ${message}`); 40 | return assert.instanceOf(error,error_match,message); 41 | } 42 | }) 43 | } 44 | 45 | function makeOrder(order) { 46 | const missingFields = requiredOrderFields.filter(field => !order.hasOwnProperty(field)); 47 | if (missingFields.length) 48 | throw new Error(`Missing fields: ${missingFields.join(', ')}`); 49 | return Object.assign({ 50 | takerAddress: NULL_ADDRESS, 51 | feeRecipientAddress: NULL_ADDRESS, 52 | senderAddress: NULL_ADDRESS, 53 | makerFee: 0, 54 | takerFee: 0, 55 | expirationTimeSeconds: new BN('115792089237316195423570985008687907853269984665640564039457584007913129639935'), 56 | salt: 0, 57 | makerFeeAssetData: "0x", 58 | takerFeeAssetData: "0x" 59 | },order); 60 | } 61 | 62 | const proxyIds = { 63 | erc20: "0xf47261b0", 64 | erc721: "0x02571792", 65 | erc1155: "0xa7cb5fb7" 66 | } 67 | 68 | function encodeAssetData(type,data) { 69 | switch (type) { 70 | case 'erc20': 71 | return proxyIds.erc20 + eth.abi.encodeParameters(['address'],[data.contractAddress]).substr(2); 72 | case 'erc721': 73 | return proxyIds.erc721 + eth.abi.encodeParameters(['address','uint256'],[data.contractAddress,data.tokenId]).substr(2); 74 | case 'erc1155': 75 | return proxyIds.erc1155 + eth.abi.encodeParameters(['address','uint256[]','uint256[]','bytes'],[data.contractAddress,[data.tokenId],[data.tokenAmount],data.extra || "0x"]).substr(2); 76 | } 77 | throw new Error(`Unknown type ${type}`); 78 | } 79 | 80 | module.exports = {NULL_ADDRESS, signTypedData, assertIsRejected, makeOrder, encodeAssetData, proxyIds}; 81 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * trufflesuite.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | // const HDWalletProvider = require('@truffle/hdwallet-provider'); 22 | // const infuraKey = "fj4jll3k....."; 23 | // 24 | // const fs = require('fs'); 25 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 26 | 27 | module.exports = { 28 | /** 29 | * Networks define how you connect to your ethereum client and let you set the 30 | * defaults web3 uses to send transactions. If you don't specify one truffle 31 | * will spin up a development blockchain for you on port 9545 when you 32 | * run `develop` or `test`. You can ask a truffle command to use a specific 33 | * network from the command line, e.g 34 | * 35 | * $ truffle test --network 36 | */ 37 | 38 | networks: { 39 | // Useful for testing. The `development` name is special - truffle uses it by default 40 | // if it's defined here and no other network is specified at the command line. 41 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 42 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 43 | // options below to some value. 44 | // 45 | development: { 46 | host: "127.0.0.1", // Localhost (default: none) 47 | port: 8545, // Standard Ethereum port (default: none) 48 | network_id: "50", // Any network (default: none) 49 | }, 50 | // Another network with more advanced options... 51 | // advanced: { 52 | // port: 8777, // Custom port 53 | // network_id: 1342, // Custom network 54 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 55 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 56 | // from:
, // Account to send txs from (default: accounts[0]) 57 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 58 | // }, 59 | // Useful for deploying to a public network. 60 | // NB: It's important to wrap the provider as a function. 61 | // ropsten: { 62 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 63 | // network_id: 3, // Ropsten's id 64 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 65 | // confirmations: 2, // # of confs to wait between deployments. (default: 0) 66 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 67 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 68 | // }, 69 | // Useful for private networks 70 | // private: { 71 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 72 | // network_id: 2111, // This network is yours, in the cloud. 73 | // production: true // Treats this network as if it was a public net. (default: false) 74 | // } 75 | }, 76 | 77 | // Set default mocha options here, use special reporters etc. 78 | mocha: { 79 | // timeout: 100000 80 | }, 81 | 82 | // Configure your compilers 83 | compilers: { 84 | solc: { 85 | version: "0.8.0", // Fetch exact version from solc-bin (default: truffle's version) 86 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 87 | settings: { // See the solidity docs for advice about optimization and evmVersion 88 | optimizer: { 89 | enabled: false, 90 | runs: 200 91 | }, 92 | } 93 | } 94 | }, 95 | 96 | // Truffle DB is currently disabled by default; to enable it, change enabled: false to enabled: true 97 | // 98 | // Note: if you migrated your contracts prior to enabling this field in your Truffle project and want 99 | // those previously migrated contracts available in the .db directory, you will need to run the following: 100 | // $ truffle migrate --reset --compile-all 101 | 102 | db: { 103 | enabled: false 104 | } 105 | }; 106 | --------------------------------------------------------------------------------