From d3db2dcfbb1c96325ae57226a8a5f9b38a917842 Mon Sep 17 00:00:00 2001 From: James Towle Date: Mon, 8 Jul 2019 21:27:17 -0500 Subject: [PATCH] `@0x:contracts-utils` Added RichErrors to LibBytes --- contracts/coordinator/test/mixins.ts | 10 +- contracts/utils/contracts/src/LibBytes.sol | 181 +++++++---- .../contracts/src/LibBytesRichErrors.sol | 59 ++++ .../utils/contracts/src/LibRichErrors.sol | 58 ++++ .../utils/contracts/test/TestLibBytes.sol | 17 +- contracts/utils/test/lib_bytes.ts | 287 ++++++++++++------ packages/utils/src/index.ts | 3 +- packages/utils/src/lib_bytes_revert_errors.ts | 30 ++ 8 files changed, 479 insertions(+), 166 deletions(-) create mode 100644 contracts/utils/contracts/src/LibBytesRichErrors.sol create mode 100644 contracts/utils/contracts/src/LibRichErrors.sol create mode 100644 packages/utils/src/lib_bytes_revert_errors.ts diff --git a/contracts/coordinator/test/mixins.ts b/contracts/coordinator/test/mixins.ts index 96292df260..ce1fe22d5d 100644 --- a/contracts/coordinator/test/mixins.ts +++ b/contracts/coordinator/test/mixins.ts @@ -13,7 +13,7 @@ import { import { BlockchainLifecycle } from '@0x/dev-utils'; import { transactionHashUtils } from '@0x/order-utils'; import { EIP712DomainWithDefaultSchema, RevertReason, SignatureType, SignedOrder } from '@0x/types'; -import { BigNumber, providerUtils } from '@0x/utils'; +import { BigNumber, LibBytesRevertErrors, providerUtils } from '@0x/utils'; import * as chai from 'chai'; import * as ethUtil from 'ethereumjs-util'; @@ -206,10 +206,12 @@ describe('Mixins tests', () => { }); it('should revert if data is less than 4 bytes long', async () => { const data = '0x010203'; - await expectContractCallFailedAsync( - mixins.decodeOrdersFromFillData.callAsync(data), - RevertReason.LibBytesGreaterOrEqualTo4LengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsFourRequired, + new BigNumber(3), // the length of data + new BigNumber(4), ); + return expect(mixins.decodeOrdersFromFillData.callAsync(data)).to.revertWith(expectedError); }); }); diff --git a/contracts/utils/contracts/src/LibBytes.sol b/contracts/utils/contracts/src/LibBytes.sol index 1c5fa7417b..d955494682 100644 --- a/contracts/utils/contracts/src/LibBytes.sol +++ b/contracts/utils/contracts/src/LibBytes.sol @@ -1,6 +1,6 @@ /* - Copyright 2018 ZeroEx Intl. + Copyright 2019 ZeroEx Intl. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ pragma solidity ^0.5.9; +import "./LibBytesRichErrors.sol"; + library LibBytes { @@ -38,7 +40,7 @@ library LibBytes { } return memoryAddress; } - + /// @dev Gets the memory address for the contents of a byte array. /// @param input Byte array to lookup. /// @return memoryAddress Memory address of the contents of the byte array. @@ -121,7 +123,7 @@ library LibBytes { source := add(source, 32) dest := add(dest, 32) } - + // Write the last 32 bytes mstore(dEnd, last) } @@ -152,7 +154,7 @@ library LibBytes { sEnd := sub(sEnd, 32) dEnd := sub(dEnd, 32) } - + // Write the first 32 bytes mstore(dest, first) } @@ -174,15 +176,23 @@ library LibBytes { pure returns (bytes memory result) { - require( - from <= to, - "FROM_LESS_THAN_TO_REQUIRED" - ); - require( - to <= b.length, - "TO_LESS_THAN_LENGTH_REQUIRED" - ); - + // Ensure that the from and to positions are valid positions for a slice within + // the byte array that is being used. + if (from > to) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors.InvalidByteOperationErrorCodes.FromLessThanOrEqualsToRequired, + from, + to + ); + } + if (to > b.length) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors.InvalidByteOperationErrorCodes.ToLessThanOrEqualsLengthRequired, + to, + b.length + ); + } + // Create a new bytes structure and copy contents result = new bytes(to - from); memCopy( @@ -192,7 +202,7 @@ library LibBytes { ); return result; } - + /// @dev Returns a slice from a byte array without preserving the input. /// @param b The byte array to take a slice from. Will be destroyed in the process. /// @param from The starting index for the slice (inclusive). @@ -208,15 +218,23 @@ library LibBytes { pure returns (bytes memory result) { - require( - from <= to, - "FROM_LESS_THAN_TO_REQUIRED" - ); - require( - to <= b.length, - "TO_LESS_THAN_LENGTH_REQUIRED" - ); - + // Ensure that the from and to positions are valid positions for a slice within + // the byte array that is being used. + if (from > to) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors.InvalidByteOperationErrorCodes.FromLessThanOrEqualsToRequired, + from, + to + ); + } + if (to > b.length) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors.InvalidByteOperationErrorCodes.ToLessThanOrEqualsLengthRequired, + to, + b.length + ); + } + // Create a new bytes structure around [from, to) in-place. assembly { result := add(b, from) @@ -233,10 +251,13 @@ library LibBytes { pure returns (bytes1 result) { - require( - b.length > 0, - "GREATER_THAN_ZERO_LENGTH_REQUIRED" - ); + if (b.length == 0) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors.InvalidByteOperationErrorCodes.LengthGreaterThanZeroRequired, + b.length, + 0 + ); + } // Store last byte. result = b[b.length - 1]; @@ -257,10 +278,13 @@ library LibBytes { pure returns (address result) { - require( - b.length >= 20, - "GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED" - ); + if (b.length < 20) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsTwentyRequired, + b.length, + 20 // 20 is length of address + ); + } // Store last 20 bytes. result = readAddress(b, b.length - 20); @@ -303,10 +327,13 @@ library LibBytes { pure returns (address result) { - require( - b.length >= index + 20, // 20 is length of address - "GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED" - ); + if (b.length < index + 20) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsTwentyRequired, + b.length, + index + 20 // 20 is length of address + ); + } // Add offset to index: // 1. Arrays are prefixed by 32-byte length parameter (add 32 to index) @@ -335,10 +362,13 @@ library LibBytes { internal pure { - require( - b.length >= index + 20, // 20 is length of address - "GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED" - ); + if (b.length < index + 20) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsTwentyRequired, + b.length, + index + 20 // 20 is length of address + ); + } // Add offset to index: // 1. Arrays are prefixed by 32-byte length parameter (add 32 to index) @@ -359,7 +389,7 @@ library LibBytes { mload(add(b, index)), 0xffffffffffffffffffffffff0000000000000000000000000000000000000000 ) - + // Make sure input address is clean. // (Solidity does not guarantee this) input := and(input, 0xffffffffffffffffffffffffffffffffffffffff) @@ -381,10 +411,13 @@ library LibBytes { pure returns (bytes32 result) { - require( - b.length >= index + 32, - "GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED" - ); + if (b.length < index + 32) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsThirtyTwoRequired, + b.length, + index + 32 + ); + } // Arrays are prefixed by a 256 bit length parameter index += 32; @@ -408,10 +441,13 @@ library LibBytes { internal pure { - require( - b.length >= index + 32, - "GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED" - ); + if (b.length < index + 32) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsThirtyTwoRequired, + b.length, + index + 32 + ); + } // Arrays are prefixed by a 256 bit length parameter index += 32; @@ -465,10 +501,13 @@ library LibBytes { pure returns (bytes4 result) { - require( - b.length >= index + 4, - "GREATER_OR_EQUAL_TO_4_LENGTH_REQUIRED" - ); + if (b.length < index + 4) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsFourRequired, + b.length, + index + 4 + ); + } // Arrays are prefixed by a 32 byte length field index += 32; @@ -503,11 +542,15 @@ library LibBytes { // Assert length of is valid, given // length of nested bytes - require( - b.length >= index + nestedBytesLength, - "GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED" - ); - + if (b.length < index + nestedBytesLength) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors + .InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsNestedBytesLengthRequired, + b.length, + index + nestedBytesLength + ); + } + // Return a pointer to the byte array as it exists inside `b` assembly { result := add(b, index) @@ -529,10 +572,14 @@ library LibBytes { { // Assert length of is valid, given // length of input - require( - b.length >= index + 32 + input.length, // 32 bytes to store length - "GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED" - ); + if (b.length < index + 32 + input.length) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors + .InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsNestedBytesLengthRequired, + b.length, + index + 32 + input.length // 32 bytes to store length + ); + } // Copy into memCopy( @@ -554,10 +601,14 @@ library LibBytes { { uint256 sourceLen = source.length; // Dest length must be >= source length, or some bytes would not be copied. - require( - dest.length >= sourceLen, - "GREATER_OR_EQUAL_TO_SOURCE_BYTES_LENGTH_REQUIRED" - ); + if (dest.length < sourceLen) { + LibBytesRichErrors.InvalidByteOperationErrorRevert( + LibBytesRichErrors + .InvalidByteOperationErrorCodes.DestinationLengthGreaterThanOrEqualSourceLengthRequired, + dest.length, + sourceLen + ); + } memCopy( dest.contentAddress(), source.contentAddress(), diff --git a/contracts/utils/contracts/src/LibBytesRichErrors.sol b/contracts/utils/contracts/src/LibBytesRichErrors.sol new file mode 100644 index 0000000000..a175c50425 --- /dev/null +++ b/contracts/utils/contracts/src/LibBytesRichErrors.sol @@ -0,0 +1,59 @@ +/* + + Copyright 2019 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.5.9; + +import "./LibRichErrors.sol"; + + +library LibBytesRichErrors { + + using LibRichErrors for *; + + enum InvalidByteOperationErrorCodes { + FromLessThanOrEqualsToRequired, + ToLessThanOrEqualsLengthRequired, + LengthGreaterThanZeroRequired, + LengthGreaterThanOrEqualsFourRequired, + LengthGreaterThanOrEqualsTwentyRequired, + LengthGreaterThanOrEqualsThirtyTwoRequired, + LengthGreaterThanOrEqualsNestedBytesLengthRequired, + DestinationLengthGreaterThanOrEqualSourceLengthRequired + } + + // bytes4(keccak256("InvalidByteOperationError(uint8,uint256,uint256)")) + bytes4 internal constant INVALID_BYTE_OPERATION_ERROR_SELECTOR = + 0x28006595; + + // solhint-disable func-name-mixedcase + function InvalidByteOperationErrorRevert( + InvalidByteOperationErrorCodes errorCode, + uint256 endpoint, + uint256 required + ) + internal + pure + { + abi.encodeWithSelector( + INVALID_BYTE_OPERATION_ERROR_SELECTOR, + errorCode, + endpoint, + required + )._rrevert(); + } +} diff --git a/contracts/utils/contracts/src/LibRichErrors.sol b/contracts/utils/contracts/src/LibRichErrors.sol new file mode 100644 index 0000000000..5b1f89b630 --- /dev/null +++ b/contracts/utils/contracts/src/LibRichErrors.sol @@ -0,0 +1,58 @@ +/* + + Copyright 2019 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.5.9; + + +library LibRichErrors { + + // bytes4(keccak256("Error(string)")) + bytes4 internal constant STANDARD_ERROR_SELECTOR = + 0x08c379a0; + + // solhint-disable func-name-mixedcase + /// @dev ABI encode a standard, string revert error payload. + /// This is the same payload that would be included by a `revert(string)` + /// solidity statement. It has the function signature `Error(string)`. + /// @param message The error string. + /// @return The ABI encoded error. + function StandardError( + string memory message + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + STANDARD_ERROR_SELECTOR, + bytes(message) + ); + } + // solhint-enable func-name-mixedcase + + /// @dev Reverts an encoded rich revert reason `errorData`. + /// @param errorData ABI encoded error data. + function _rrevert(bytes memory errorData) + internal + pure + { + assembly { + revert(add(errorData, 0x20), mload(errorData)) + } + } +} diff --git a/contracts/utils/contracts/test/TestLibBytes.sol b/contracts/utils/contracts/test/TestLibBytes.sol index 5890648eba..560c896593 100644 --- a/contracts/utils/contracts/test/TestLibBytes.sol +++ b/contracts/utils/contracts/test/TestLibBytes.sol @@ -22,9 +22,20 @@ import "../src/LibBytes.sol"; contract TestLibBytes { - + using LibBytes for bytes; + /// @dev Pops the last byte off of a byte array by modifying its length. + /// @param b Byte array that will be modified. + /// @return The byte that was popped off. + function publicPopLastByteStateful(bytes memory b) + public + returns (bytes memory, bytes1 result) + { + result = b.popLastByte(); + return (b, result); + } + /// @dev Pops the last byte off of a byte array by modifying its length. /// @param b Byte array that will be modified. /// @return The byte that was popped off. @@ -61,7 +72,7 @@ contract TestLibBytes { equal = lhs.equals(rhs); return equal; } - + function publicEqualsPop1(bytes memory lhs, bytes memory rhs) public pure @@ -236,7 +247,7 @@ contract TestLibBytes { b.writeBytesWithLength(index, input); return b; } - + /// @dev Copies a block of memory from one location to another. /// @param mem Memory contents we want to apply memCopy to /// @param dest Destination offset into . diff --git a/contracts/utils/test/lib_bytes.ts b/contracts/utils/test/lib_bytes.ts index cedcb9d80a..1dfd95fc00 100644 --- a/contracts/utils/test/lib_bytes.ts +++ b/contracts/utils/test/lib_bytes.ts @@ -1,16 +1,7 @@ -import { - chaiSetup, - constants, - expectContractCallFailedAsync, - provider, - txDefaults, - typeEncodingUtils, - web3Wrapper, -} from '@0x/contracts-test-utils'; +import { chaiSetup, constants, provider, txDefaults, typeEncodingUtils, web3Wrapper } from '@0x/contracts-test-utils'; import { BlockchainLifecycle } from '@0x/dev-utils'; import { generatePseudoRandomSalt } from '@0x/order-utils'; -import { RevertReason } from '@0x/types'; -import { BigNumber } from '@0x/utils'; +import { BigNumber, LibBytesRevertErrors } from '@0x/utils'; import BN = require('bn.js'); import * as chai from 'chai'; import ethUtil = require('ethereumjs-util'); @@ -106,10 +97,12 @@ describe('LibBytes', () => { describe('popLastByte', () => { it('should revert if length is 0', async () => { - return expectContractCallFailedAsync( - libBytes.publicPopLastByte.callAsync(constants.NULL_BYTES), - RevertReason.LibBytesGreaterThanZeroLengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanZeroRequired, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, ); + return expect(libBytes.publicPopLastByte.callAsync(constants.NULL_BYTES)).to.revertWith(expectedError); }); it('should pop the last byte from the input and return it when array holds more than 1 byte', async () => { const [newBytes, poppedByte] = await libBytes.publicPopLastByte.callAsync(byteArrayLongerThan32Bytes); @@ -128,9 +121,14 @@ describe('LibBytes', () => { describe('popLast20Bytes', () => { it('should revert if length is less than 20', async () => { - return expectContractCallFailedAsync( - libBytes.publicPopLast20Bytes.callAsync(byteArrayShorterThan20Bytes), - RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + const byteLen = (byteArrayShorterThan20Bytes.length - 2) / 2; + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsTwentyRequired, + new BigNumber(byteLen), // length of byteArrayShorterThan20Byte + new BigNumber(20), + ); + return expect(libBytes.publicPopLast20Bytes.callAsync(byteArrayShorterThan20Bytes)).to.revertWith( + expectedError, ); }); it('should pop the last 20 bytes from the input and return it when array holds more than 20 bytes', async () => { @@ -203,10 +201,16 @@ describe('LibBytes', () => { describe('deepCopyBytes', () => { it('should revert if dest is shorter than source', async () => { - return expectContractCallFailedAsync( - libBytes.publicDeepCopyBytes.callAsync(byteArrayShorterThan32Bytes, byteArrayLongerThan32Bytes), - RevertReason.LibBytesGreaterOrEqualToSourceBytesLengthRequired, + const endpointByteLen = (byteArrayShorterThan20Bytes.length - 2) / 2; + const requiredByteLen = (byteArrayLongerThan32Bytes.length - 2) / 2; + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.DestinationLengthGreaterThanOrEqualSourceLengthRequired, + new BigNumber(endpointByteLen), // length of byteArrayShorterThan20Byte + new BigNumber(requiredByteLen), // length of byteArrayShorterThan20Byte ); + return expect( + libBytes.publicDeepCopyBytes.callAsync(byteArrayShorterThan32Bytes, byteArrayLongerThan32Bytes), + ).to.revertWith(expectedError); }); it('should overwrite dest with source if source and dest have equal length', async () => { const zeroedByteArrayLongerThan32Bytes = `0x${_.repeat('0', byteArrayLongerThan32Bytes.length - 2)}`; @@ -256,18 +260,22 @@ describe('LibBytes', () => { it('should fail if the byte array is too short to hold an address', async () => { const shortByteArray = '0xabcdef'; const offset = new BigNumber(0); - return expectContractCallFailedAsync( - libBytes.publicReadAddress.callAsync(shortByteArray, offset), - RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsTwentyRequired, + new BigNumber(3), + new BigNumber(20), ); + return expect(libBytes.publicReadAddress.callAsync(shortByteArray, offset)).to.revertWith(expectedError); }); it('should fail if the length between the offset and end of the byte array is too short to hold an address', async () => { const byteArray = testAddress; const badOffset = new BigNumber(ethUtil.toBuffer(byteArray).byteLength); - return expectContractCallFailedAsync( - libBytes.publicReadAddress.callAsync(byteArray, badOffset), - RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsTwentyRequired, + new BigNumber(20), + new BigNumber(40), ); + return expect(libBytes.publicReadAddress.callAsync(byteArray, badOffset)).to.revertWith(expectedError); }); }); @@ -300,17 +308,26 @@ describe('LibBytes', () => { }); it('should fail if the byte array is too short to hold an address', async () => { const offset = new BigNumber(0); - return expectContractCallFailedAsync( - libBytes.publicWriteAddress.callAsync(byteArrayShorterThan20Bytes, offset, testAddress), - RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + const byteLen = ethUtil.toBuffer(byteArrayShorterThan20Bytes).byteLength; + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsTwentyRequired, + new BigNumber(byteLen), + new BigNumber(20), ); + return expect( + libBytes.publicWriteAddress.callAsync(byteArrayShorterThan20Bytes, offset, testAddress), + ).to.revertWith(expectedError); }); it('should fail if the length between the offset and end of the byte array is too short to hold an address', async () => { const byteArray = byteArrayLongerThan32Bytes; const badOffset = new BigNumber(ethUtil.toBuffer(byteArray).byteLength); - return expectContractCallFailedAsync( - libBytes.publicWriteAddress.callAsync(byteArray, badOffset, testAddress), - RevertReason.LibBytesGreaterOrEqualTo20LengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsTwentyRequired, + badOffset, + badOffset.plus(new BigNumber(20)), + ); + return expect(libBytes.publicWriteAddress.callAsync(byteArray, badOffset, testAddress)).to.revertWith( + expectedError, ); }); }); @@ -332,17 +349,24 @@ describe('LibBytes', () => { }); it('should fail if the byte array is too short to hold a bytes32', async () => { const offset = new BigNumber(0); - return expectContractCallFailedAsync( - libBytes.publicReadBytes32.callAsync(byteArrayShorterThan32Bytes, offset), - RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + const byteLen = new BigNumber((byteArrayShorterThan32Bytes.length - 2) / 2); + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsThirtyTwoRequired, + byteLen, + new BigNumber(32), + ); + return expect(libBytes.publicReadBytes32.callAsync(byteArrayShorterThan32Bytes, offset)).to.revertWith( + expectedError, ); }); it('should fail if the length between the offset and end of the byte array is too short to hold a bytes32', async () => { const badOffset = new BigNumber(ethUtil.toBuffer(testBytes32).byteLength); - return expectContractCallFailedAsync( - libBytes.publicReadBytes32.callAsync(testBytes32, badOffset), - RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsThirtyTwoRequired, + badOffset, + badOffset.plus(new BigNumber(32)), ); + return expect(libBytes.publicReadBytes32.callAsync(testBytes32, badOffset)).to.revertWith(expectedError); }); }); @@ -375,17 +399,26 @@ describe('LibBytes', () => { }); it('should fail if the byte array is too short to hold a bytes32', async () => { const offset = new BigNumber(0); - return expectContractCallFailedAsync( - libBytes.publicWriteBytes32.callAsync(byteArrayShorterThan32Bytes, offset, testBytes32), - RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + const byteLen = new BigNumber((byteArrayShorterThan32Bytes.length - 2) / 2); + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsThirtyTwoRequired, + byteLen, + new BigNumber(32), ); + return expect( + libBytes.publicWriteBytes32.callAsync(byteArrayShorterThan32Bytes, offset, testBytes32), + ).to.revertWith(expectedError); }); it('should fail if the length between the offset and end of the byte array is too short to hold a bytes32', async () => { const byteArray = byteArrayLongerThan32Bytes; const badOffset = new BigNumber(ethUtil.toBuffer(byteArray).byteLength); - return expectContractCallFailedAsync( - libBytes.publicWriteBytes32.callAsync(byteArray, badOffset, testBytes32), - RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsThirtyTwoRequired, + badOffset, + badOffset.plus(new BigNumber(32)), + ); + return expect(libBytes.publicWriteBytes32.callAsync(byteArray, badOffset, testBytes32)).to.revertWith( + expectedError, ); }); }); @@ -411,9 +444,14 @@ describe('LibBytes', () => { }); it('should fail if the byte array is too short to hold a uint256', async () => { const offset = new BigNumber(0); - return expectContractCallFailedAsync( - libBytes.publicReadUint256.callAsync(byteArrayShorterThan32Bytes, offset), - RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + const byteLen = new BigNumber((byteArrayShorterThan32Bytes.length - 2) / 2); + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsThirtyTwoRequired, + byteLen, + new BigNumber(32), + ); + return expect(libBytes.publicReadUint256.callAsync(byteArrayShorterThan32Bytes, offset)).to.revertWith( + expectedError, ); }); it('should fail if the length between the offset and end of the byte array is too short to hold a uint256', async () => { @@ -421,10 +459,12 @@ describe('LibBytes', () => { const testUint256AsBuffer = ethUtil.toBuffer(formattedTestUint256); const byteArray = ethUtil.bufferToHex(testUint256AsBuffer); const badOffset = new BigNumber(testUint256AsBuffer.byteLength); - return expectContractCallFailedAsync( - libBytes.publicReadUint256.callAsync(byteArray, badOffset), - RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsThirtyTwoRequired, + badOffset, + badOffset.plus(new BigNumber(32)), ); + return expect(libBytes.publicReadUint256.callAsync(byteArray, badOffset)).to.revertWith(expectedError); }); }); @@ -461,17 +501,26 @@ describe('LibBytes', () => { }); it('should fail if the byte array is too short to hold a uint256', async () => { const offset = new BigNumber(0); - return expectContractCallFailedAsync( - libBytes.publicWriteUint256.callAsync(byteArrayShorterThan32Bytes, offset, testUint256), - RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + const byteLen = new BigNumber((byteArrayShorterThan32Bytes.length - 2) / 2); + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsThirtyTwoRequired, + byteLen, + new BigNumber(32), ); + return expect( + libBytes.publicWriteUint256.callAsync(byteArrayShorterThan32Bytes, offset, testUint256), + ).to.revertWith(expectedError); }); it('should fail if the length between the offset and end of the byte array is too short to hold a uint256', async () => { const byteArray = byteArrayLongerThan32Bytes; const badOffset = new BigNumber(ethUtil.toBuffer(byteArray).byteLength); - return expectContractCallFailedAsync( - libBytes.publicWriteUint256.callAsync(byteArray, badOffset, testUint256), - RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsThirtyTwoRequired, + badOffset, + badOffset.plus(new BigNumber(32)), + ); + return expect(libBytes.publicWriteUint256.callAsync(byteArray, badOffset, testUint256)).to.revertWith( + expectedError, ); }); }); @@ -480,10 +529,15 @@ describe('LibBytes', () => { // AssertionError: expected promise to be rejected with an error including 'revert' but it was fulfilled with '0x08c379a0' it('should revert if byte array has a length < 4', async () => { const byteArrayLessThan4Bytes = '0x010101'; + const byteLen = (byteArrayLessThan4Bytes.length - 2) / 2; const offset = new BigNumber(0); - return expectContractCallFailedAsync( - libBytes.publicReadBytes4.callAsync(byteArrayLessThan4Bytes, offset), - RevertReason.LibBytesGreaterOrEqualTo4LengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsFourRequired, + new BigNumber(byteLen), // length of byteArrayLessThan4Bytes + new BigNumber(4), + ); + return expect(libBytes.publicReadBytes4.callAsync(byteArrayLessThan4Bytes, offset)).to.revertWith( + expectedError, ); }); it('should return the first 4 bytes of a byte array of arbitrary length', async () => { @@ -507,10 +561,13 @@ describe('LibBytes', () => { }); it('should fail if the length between the offset and end of the byte array is too short to hold a bytes4', async () => { const badOffset = new BigNumber(ethUtil.toBuffer(testBytes4).byteLength); - return expectContractCallFailedAsync( - libBytes.publicReadBytes4.callAsync(testBytes4, badOffset), - RevertReason.LibBytesGreaterOrEqualTo4LengthRequired, + const byteLen = new BigNumber((testBytes4.length - 2) / 2); + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsFourRequired, + byteLen, + badOffset.plus(new BigNumber(4)), ); + return expect(libBytes.publicReadBytes4.callAsync(testBytes4, badOffset)).to.revertWith(expectedError); }); }); @@ -557,30 +614,48 @@ describe('LibBytes', () => { it('should fail if the byte array is too short to hold the length of a nested byte array', async () => { // The length of the nested array is 32 bytes. By storing less than 32 bytes, a length cannot be read. const offset = new BigNumber(0); - return expectContractCallFailedAsync( - libBytes.publicReadBytesWithLength.callAsync(byteArrayShorterThan32Bytes, offset), - RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + const byteLen = new BigNumber((byteArrayShorterThan32Bytes.length - 2) / 2); + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsThirtyTwoRequired, + byteLen, + new BigNumber(32), ); + return expect( + libBytes.publicReadBytesWithLength.callAsync(byteArrayShorterThan32Bytes, offset), + ).to.revertWith(expectedError); }); it('should fail if we store a nested byte array length, without a nested byte array', async () => { const offset = new BigNumber(0); - return expectContractCallFailedAsync( - libBytes.publicReadBytesWithLength.callAsync(testBytes32, offset), - RevertReason.LibBytesGreaterOrEqualToNestedBytesLengthRequired, + const byteLen = new BigNumber((testBytes32.length - 2) / 2); + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsNestedBytesLengthRequired, + byteLen, + (new BigNumber(testBytes32)).plus(32), + ); + return expect(libBytes.publicReadBytesWithLength.callAsync(testBytes32, offset)).to.revertWith( + expectedError, ); }); it('should fail if the length between the offset and end of the byte array is too short to hold the length of a nested byte array', async () => { const badOffset = new BigNumber(ethUtil.toBuffer(byteArrayShorterThan32Bytes).byteLength); - return expectContractCallFailedAsync( - libBytes.publicReadBytesWithLength.callAsync(byteArrayShorterThan32Bytes, badOffset), - RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsThirtyTwoRequired, + badOffset, + badOffset.plus(new BigNumber(32)), ); + return expect( + libBytes.publicReadBytesWithLength.callAsync(byteArrayShorterThan32Bytes, badOffset), + ).to.revertWith(expectedError); }); it('should fail if the length between the offset and end of the byte array is too short to hold the nested byte array', async () => { const badOffset = new BigNumber(ethUtil.toBuffer(testBytes32).byteLength); - return expectContractCallFailedAsync( - libBytes.publicReadBytesWithLength.callAsync(testBytes32, badOffset), - RevertReason.LibBytesGreaterOrEqualTo32LengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsThirtyTwoRequired, + badOffset, + badOffset.plus(new BigNumber(32)), + ); + return expect(libBytes.publicReadBytesWithLength.callAsync(testBytes32, badOffset)).to.revertWith( + expectedError, ); }); }); @@ -690,18 +765,28 @@ describe('LibBytes', () => { it('should fail if the byte array is too short to hold the length of a nested byte array', async () => { const offset = new BigNumber(0); const emptyByteArray = ethUtil.bufferToHex(new Buffer(1)); - return expectContractCallFailedAsync( - libBytes.publicWriteBytesWithLength.callAsync(emptyByteArray, offset, longData), - RevertReason.LibBytesGreaterOrEqualToNestedBytesLengthRequired, + const inputLen = new BigNumber((longData.length - 2) / 2); + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsNestedBytesLengthRequired, + new BigNumber(1), + new BigNumber(32).plus(inputLen), ); + return expect( + libBytes.publicWriteBytesWithLength.callAsync(emptyByteArray, offset, longData), + ).to.revertWith(expectedError); }); it('should fail if the length between the offset and end of the byte array is too short to hold the length of a nested byte array', async () => { const emptyByteArray = ethUtil.bufferToHex(new Buffer(shortTestBytesAsBuffer.byteLength)); const badOffset = new BigNumber(ethUtil.toBuffer(shortTestBytesAsBuffer).byteLength); - return expectContractCallFailedAsync( - libBytes.publicWriteBytesWithLength.callAsync(emptyByteArray, badOffset, shortData), - RevertReason.LibBytesGreaterOrEqualToNestedBytesLengthRequired, + const inputLen = new BigNumber((shortData.length - 2) / 2); + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsNestedBytesLengthRequired, + badOffset, + badOffset.plus(new BigNumber(32)).plus(inputLen), ); + return expect( + libBytes.publicWriteBytesWithLength.callAsync(emptyByteArray, badOffset, shortData), + ).to.revertWith(expectedError); }); }); @@ -875,9 +960,13 @@ describe('LibBytes', () => { it('should revert if from > to', async () => { const from = new BigNumber(1); const to = new BigNumber(0); - expectContractCallFailedAsync( - libBytes.publicSlice.callAsync(byteArrayLongerThan32Bytes, from, to), - RevertReason.FromLessThanToRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.FromLessThanOrEqualsToRequired, + from, + to, + ); + return expect(libBytes.publicSlice.callAsync(byteArrayLongerThan32Bytes, from, to)).to.revertWith( + expectedError, ); }); it('should return a byte array of length 0 if from == to', async () => { @@ -896,12 +985,16 @@ describe('LibBytes', () => { expect(result).to.eq(constants.NULL_BYTES); }); it('should revert if to > input.length', async () => { - const byteLen = (byteArrayLongerThan32Bytes.length - 2) / 2; + const byteLen: number = (byteArrayLongerThan32Bytes.length - 2) / 2; const from = new BigNumber(0); const to = new BigNumber(byteLen).plus(1); - expectContractCallFailedAsync( - libBytes.publicSlice.callAsync(byteArrayLongerThan32Bytes, from, to), - RevertReason.ToLessThanLengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.ToLessThanOrEqualsLengthRequired, + to, + new BigNumber(byteLen), + ); + return expect(libBytes.publicSlice.callAsync(byteArrayLongerThan32Bytes, from, to)).to.revertWith( + expectedError, ); }); it('should slice a section of the input', async () => { @@ -926,9 +1019,13 @@ describe('LibBytes', () => { it('should revert if from > to', async () => { const from = new BigNumber(1); const to = new BigNumber(0); - expectContractCallFailedAsync( - libBytes.publicSliceDestructive.callAsync(byteArrayLongerThan32Bytes, from, to), - RevertReason.FromLessThanToRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.FromLessThanOrEqualsToRequired, + from, + to, + ); + return expect(libBytes.publicSlice.callAsync(byteArrayLongerThan32Bytes, from, to)).to.revertWith( + expectedError, ); }); it('should return a byte array of length 0 if from == to', async () => { @@ -948,10 +1045,14 @@ describe('LibBytes', () => { const byteLen = (byteArrayLongerThan32Bytes.length - 2) / 2; const from = new BigNumber(0); const to = new BigNumber(byteLen).plus(1); - expectContractCallFailedAsync( - libBytes.publicSliceDestructive.callAsync(byteArrayLongerThan32Bytes, from, to), - RevertReason.ToLessThanLengthRequired, + const expectedError = new LibBytesRevertErrors.InvalidByteOperationError( + LibBytesRevertErrors.InvalidByteOperationErrorCodes.ToLessThanOrEqualsLengthRequired, + to, + new BigNumber(byteLen), ); + return expect( + libBytes.publicSliceDestructive.callAsync(byteArrayLongerThan32Bytes, from, to), + ).to.revertWith(expectedError); }); it('should slice a section of the input', async () => { const from = new BigNumber(1); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 0197df657b..77c2b7de22 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,3 +1,4 @@ +import * as LibBytesRevertErrors from './lib_bytes_revert_errors'; import * as OwnableRevertErrors from './ownable_revert_errors'; import * as SafeMathRevertErrors from './safe_math_revert_errors'; @@ -28,4 +29,4 @@ export { AnyRevertError, } from './revert_error'; -export { OwnableRevertErrors, SafeMathRevertErrors }; +export { LibBytesRevertErrors, OwnableRevertErrors, SafeMathRevertErrors }; diff --git a/packages/utils/src/lib_bytes_revert_errors.ts b/packages/utils/src/lib_bytes_revert_errors.ts new file mode 100644 index 0000000000..bbefed2764 --- /dev/null +++ b/packages/utils/src/lib_bytes_revert_errors.ts @@ -0,0 +1,30 @@ +import { BigNumber } from './configured_bignumber'; +import { RevertError } from './revert_error'; + +export enum InvalidByteOperationErrorCodes { + FromLessThanOrEqualsToRequired, + ToLessThanOrEqualsLengthRequired, + LengthGreaterThanZeroRequired, + LengthGreaterThanOrEqualsFourRequired, + LengthGreaterThanOrEqualsTwentyRequired, + LengthGreaterThanOrEqualsThirtyTwoRequired, + LengthGreaterThanOrEqualsNestedBytesLengthRequired, + DestinationLengthGreaterThanOrEqualSourceLengthRequired, +} + +export class InvalidByteOperationError extends RevertError { + constructor(error?: InvalidByteOperationErrorCodes, endpoint?: BigNumber, required?: BigNumber) { + super( + 'InvalidByteOperationError', + 'InvalidByteOperationError(uint8 error, uint256 endpoint, uint256 required)', + { + error, + endpoint, + required, + }, + ); + } +} + +// Register the InvalidByteOperationError type +RevertError.registerType(InvalidByteOperationError);