diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index 56619891b4..d546d3d0c5 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "3.2.0", + "changes": [ + { + "note": "Disallow the zero address from being made an authorized address in MixinAuthorizable, and created an archive directory that includes an old version of Ownable", + "pr": 2019 + } + ] + }, { "timestamp": 1563193019, "version": "2.2.2", diff --git a/contracts/asset-proxy/compiler.json b/contracts/asset-proxy/compiler.json index 3a1115f5e4..51ff4efa9b 100644 --- a/contracts/asset-proxy/compiler.json +++ b/contracts/asset-proxy/compiler.json @@ -24,10 +24,10 @@ } }, "contracts": [ + "archive/MixinAuthorizable.sol", "src/ERC1155Proxy.sol", "src/ERC20Proxy.sol", "src/ERC721Proxy.sol", - "src/MixinAuthorizable.sol", "src/MultiAssetProxy.sol", "src/StaticCallProxy.sol", "src/interfaces/IAssetData.sol", diff --git a/contracts/asset-proxy/contracts/src/MixinAssetProxyDispatcher.sol b/contracts/asset-proxy/contracts/archive/MixinAssetProxyDispatcher.sol similarity index 95% rename from contracts/asset-proxy/contracts/src/MixinAssetProxyDispatcher.sol rename to contracts/asset-proxy/contracts/archive/MixinAssetProxyDispatcher.sol index 804b0a8686..bbc93ec6cb 100644 --- a/contracts/asset-proxy/contracts/src/MixinAssetProxyDispatcher.sol +++ b/contracts/asset-proxy/contracts/archive/MixinAssetProxyDispatcher.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,9 +18,9 @@ pragma solidity ^0.5.9; -import "@0x/contracts-utils/contracts/src/Ownable.sol"; -import "./interfaces/IAssetProxy.sol"; -import "./interfaces/IAssetProxyDispatcher.sol"; +import "../archive/Ownable.sol"; +import "../src/interfaces/IAssetProxy.sol"; +import "../src/interfaces/IAssetProxyDispatcher.sol"; contract MixinAssetProxyDispatcher is @@ -84,7 +84,7 @@ contract MixinAssetProxyDispatcher is assetData.length > 3, "LENGTH_GREATER_THAN_3_REQUIRED" ); - + // Lookup assetProxy. We do not use `LibBytes.readBytes4` for gas efficiency reasons. bytes4 assetProxyId; assembly { @@ -100,10 +100,10 @@ contract MixinAssetProxyDispatcher is assetProxy != address(0), "ASSET_PROXY_DOES_NOT_EXIST" ); - + // We construct calldata for the `assetProxy.transferFrom` ABI. // The layout of this calldata is in the table below. - // + // // | Area | Offset | Length | Contents | // | -------- |--------|---------|-------------------------------------------- | // | Header | 0 | 4 | function selector | @@ -127,12 +127,12 @@ contract MixinAssetProxyDispatcher is // `cdEnd` is the end of the calldata for `assetProxy.transferFrom`. let cdEnd := add(cdStart, add(132, dataAreaLength)) - + /////// Setup Header Area /////// // This area holds the 4-byte `transferFromSelector`. // bytes4(keccak256("transferFrom(bytes,address,address,uint256)")) = 0xa85e59e4 mstore(cdStart, 0xa85e59e400000000000000000000000000000000000000000000000000000000) - + /////// Setup Params Area /////// // Each parameter is padded to 32-bytes. The entire Params Area is 128 bytes. // Notes: @@ -142,7 +142,7 @@ contract MixinAssetProxyDispatcher is mstore(add(cdStart, 36), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) mstore(add(cdStart, 68), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) mstore(add(cdStart, 100), amount) - + /////// Setup Data Area /////// // This area holds `assetData`. let dataArea := add(cdStart, 132) @@ -159,7 +159,7 @@ contract MixinAssetProxyDispatcher is assetProxy, // call address of asset proxy 0, // don't send any ETH cdStart, // pointer to start of input - sub(cdEnd, cdStart), // length of input + sub(cdEnd, cdStart), // length of input cdStart, // write output over input 512 // reserve 512 bytes for output ) diff --git a/contracts/asset-proxy/contracts/src/MixinAuthorizable.sol b/contracts/asset-proxy/contracts/archive/MixinAuthorizable.sol similarity index 96% rename from contracts/asset-proxy/contracts/src/MixinAuthorizable.sol rename to contracts/asset-proxy/contracts/archive/MixinAuthorizable.sol index 8fa2154521..5efb1340b6 100644 --- a/contracts/asset-proxy/contracts/src/MixinAuthorizable.sol +++ b/contracts/asset-proxy/contracts/archive/MixinAuthorizable.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,8 +18,8 @@ pragma solidity ^0.5.9; -import "@0x/contracts-utils/contracts/src/Ownable.sol"; -import "./interfaces/IAuthorizable.sol"; +import "../archive/Ownable.sol"; +import "../src/interfaces/IAuthorizable.sol"; contract MixinAuthorizable is diff --git a/contracts/asset-proxy/contracts/archive/Ownable.sol b/contracts/asset-proxy/contracts/archive/Ownable.sol new file mode 100644 index 0000000000..b15e5d0266 --- /dev/null +++ b/contracts/asset-proxy/contracts/archive/Ownable.sol @@ -0,0 +1,33 @@ +pragma solidity ^0.5.9; + +import "@0x/contracts-utils/contracts/src/interfaces/IOwnable.sol"; + + +contract Ownable is + IOwnable +{ + address public owner; + + constructor () + public + { + owner = msg.sender; + } + + modifier onlyOwner() { + require( + msg.sender == owner, + "ONLY_CONTRACT_OWNER" + ); + _; + } + + function transferOwnership(address newOwner) + public + onlyOwner + { + if (newOwner != address(0)) { + owner = newOwner; + } + } +} diff --git a/contracts/asset-proxy/contracts/src/ERC1155Proxy.sol b/contracts/asset-proxy/contracts/src/ERC1155Proxy.sol index a4c649d2d4..c7879761dc 100644 --- a/contracts/asset-proxy/contracts/src/ERC1155Proxy.sol +++ b/contracts/asset-proxy/contracts/src/ERC1155Proxy.sol @@ -21,7 +21,7 @@ pragma solidity ^0.5.9; import "@0x/contracts-utils/contracts/src/LibBytes.sol"; import "@0x/contracts-utils/contracts/src/SafeMath.sol"; import "@0x/contracts-erc1155/contracts/src/interfaces/IERC1155.sol"; -import "./MixinAuthorizable.sol"; +import "../archive/MixinAuthorizable.sol"; import "./interfaces/IAssetProxy.sol"; @@ -69,7 +69,7 @@ contract ERC1155Proxy is for (uint256 i = 0; i != length; i++) { // We write the scaled values to an unused location in memory in order // to avoid copying over `ids` or `data`. This is possible if they are - // identical to `values` and the offsets for each are pointing to the + // identical to `values` and the offsets for each are pointing to the // same location in the ABI encoded calldata. scaledValues[i] = _safeMul(values[i], amount); } diff --git a/contracts/asset-proxy/contracts/src/ERC20Proxy.sol b/contracts/asset-proxy/contracts/src/ERC20Proxy.sol index c8d269f5a9..01a15ff6b6 100644 --- a/contracts/asset-proxy/contracts/src/ERC20Proxy.sol +++ b/contracts/asset-proxy/contracts/src/ERC20Proxy.sol @@ -18,7 +18,7 @@ pragma solidity ^0.5.9; -import "./MixinAuthorizable.sol"; +import "../archive/MixinAuthorizable.sol"; contract ERC20Proxy is @@ -26,9 +26,9 @@ contract ERC20Proxy is { // Id of this proxy. bytes4 constant internal PROXY_ID = bytes4(keccak256("ERC20Token(address)")); - + // solhint-disable-next-line payable-fallback - function () + function () external { assembly { @@ -117,13 +117,13 @@ contract ERC20Proxy is // * The "token address" is offset 32+4=36 bytes into "assetData" (tables 1 & 2). // [tokenOffset = assetDataOffsetFromHeader + 36 = calldataload(4) + 4 + 36] let token := calldataload(add(calldataload(4), 40)) - + /////// Setup Header Area /////// // This area holds the 4-byte `transferFrom` selector. // Any trailing data in transferFromSelector will be // overwritten in the next `mstore` call. mstore(0, 0x23b872dd00000000000000000000000000000000000000000000000000000000) - + /////// Setup Params Area /////// // We copy the fields `from`, `to` and `amount` in bulk // from our own calldata to the new calldata. @@ -147,7 +147,7 @@ contract ERC20Proxy is // If the token does return data, we require that it is a single // nonzero 32 bytes value. // So the transfer succeeded if the call succeeded and either - // returned nothing, or returned a non-zero 32 byte value. + // returned nothing, or returned a non-zero 32 byte value. success := and(success, or( iszero(returndatasize), and( @@ -158,7 +158,7 @@ contract ERC20Proxy is if success { return(0, 0) } - + // Revert with `Error("TRANSFER_FAILED")` mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) diff --git a/contracts/asset-proxy/contracts/src/ERC721Proxy.sol b/contracts/asset-proxy/contracts/src/ERC721Proxy.sol index b22c2336ec..649c4aabdb 100644 --- a/contracts/asset-proxy/contracts/src/ERC721Proxy.sol +++ b/contracts/asset-proxy/contracts/src/ERC721Proxy.sol @@ -18,7 +18,7 @@ pragma solidity ^0.5.9; -import "./MixinAuthorizable.sol"; +import "../archive/MixinAuthorizable.sol"; contract ERC721Proxy is @@ -28,7 +28,7 @@ contract ERC721Proxy is bytes4 constant internal PROXY_ID = bytes4(keccak256("ERC721Token(address,uint256)")); // solhint-disable-next-line payable-fallback - function () + function () external { assembly { @@ -93,10 +93,10 @@ contract ERC721Proxy is // | Params | | 2 * 32 | function parameters: | // | | 4 | 12 + 20 | 1. token address | // | | 36 | | 2. tokenId | - + // We construct calldata for the `token.transferFrom` ABI. // The layout of this calldata is in the table below. - // + // // | Area | Offset | Length | Contents | // |----------|--------|---------|-------------------------------------| // | Header | 0 | 4 | function selector | @@ -121,7 +121,7 @@ contract ERC721Proxy is // Any trailing data in transferFromSelector will be // overwritten in the next `mstore` call. mstore(0, 0x23b872dd00000000000000000000000000000000000000000000000000000000) - + /////// Setup Params Area /////// // We copy the fields `from` and `to` in bulk // from our own calldata to the new calldata. @@ -145,7 +145,7 @@ contract ERC721Proxy is if success { return(0, 0) } - + // Revert with `Error("TRANSFER_FAILED")` mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) diff --git a/contracts/asset-proxy/contracts/src/MultiAssetProxy.sol b/contracts/asset-proxy/contracts/src/MultiAssetProxy.sol index a5a1a05a4b..61c0c83646 100644 --- a/contracts/asset-proxy/contracts/src/MultiAssetProxy.sol +++ b/contracts/asset-proxy/contracts/src/MultiAssetProxy.sol @@ -18,8 +18,8 @@ pragma solidity ^0.5.9; -import "./MixinAssetProxyDispatcher.sol"; -import "./MixinAuthorizable.sol"; +import "../archive/MixinAssetProxyDispatcher.sol"; +import "../archive/MixinAuthorizable.sol"; contract MultiAssetProxy is @@ -105,7 +105,7 @@ contract MultiAssetProxy is // | | 36 | | 2. offset to nestedAssetData (*) | // | Data | | | amounts: | // | | 68 | 32 | amounts Length | - // | | 100 | a | amounts Contents | + // | | 100 | a | amounts Contents | // | | | | nestedAssetData: | // | | 100 + a | 32 | nestedAssetData Length | // | | 132 + a | b | nestedAssetData Contents (offsets) | @@ -149,8 +149,8 @@ contract MultiAssetProxy is // + 32 (amounts offset) let nestedAssetDataOffset := calldataload(add(assetDataOffset, 68)) - // In order to find the start of the `amounts` contents, we must add: - // assetDataOffset + // In order to find the start of the `amounts` contents, we must add: + // assetDataOffset // + 32 (assetData len) // + 4 (assetProxyId) // + amountsOffset @@ -160,8 +160,8 @@ contract MultiAssetProxy is // Load number of elements in `amounts` let amountsLen := calldataload(sub(amountsContentsStart, 32)) - // In order to find the start of the `nestedAssetData` contents, we must add: - // assetDataOffset + // In order to find the start of the `nestedAssetData` contents, we must add: + // assetDataOffset // + 32 (assetData len) // + 4 (assetProxyId) // + nestedAssetDataOffset @@ -190,10 +190,10 @@ contract MultiAssetProxy is // Overwrite existing offset to `assetData` with our own mstore(4, 128) - + // Load `amount` let amount := calldataload(100) - + // Calculate number of bytes in `amounts` contents let amountsByteLen := mul(amountsLen, 32) @@ -208,7 +208,7 @@ contract MultiAssetProxy is let amountsElement := calldataload(add(amountsContentsStart, i)) let totalAmount := mul(amountsElement, amount) - // Revert if `amount` != 0 and multiplication resulted in an overflow + // Revert if `amount` != 0 and multiplication resulted in an overflow if iszero(or( iszero(amount), eq(div(totalAmount, amount), amountsElement) @@ -228,7 +228,7 @@ contract MultiAssetProxy is let nestedAssetDataElementOffset := calldataload(add(nestedAssetDataContentsStart, i)) // In order to find the start of the `nestedAssetData[i]` contents, we must add: - // assetDataOffset + // assetDataOffset // + 32 (assetData len) // + 4 (assetProxyId) // + nestedAssetDataOffset @@ -274,7 +274,7 @@ contract MultiAssetProxy is mstore(164, assetProxies_slot) assetProxy := sload(keccak256(132, 64)) } - + // Revert if AssetProxy with given id does not exist if iszero(assetProxy) { // Revert with `Error("ASSET_PROXY_DOES_NOT_EXIST")` @@ -284,7 +284,7 @@ contract MultiAssetProxy is mstore(96, 0) revert(0, 100) } - + // Copy `nestedAssetData[i]` from calldata to memory calldatacopy( 132, // memory slot after `amounts[i]` @@ -298,7 +298,7 @@ contract MultiAssetProxy is assetProxy, // call address of asset proxy 0, // don't send any ETH 0, // pointer to start of input - add(164, nestedAssetDataElementLen), // length of input + add(164, nestedAssetDataElementLen), // length of input 0, // write output over memory that won't be reused 0 // don't copy output to memory ) diff --git a/contracts/asset-proxy/contracts/src/interfaces/IAuthorizable.sol b/contracts/asset-proxy/contracts/src/interfaces/IAuthorizable.sol index 517e5d69bf..5def85864b 100644 --- a/contracts/asset-proxy/contracts/src/interfaces/IAuthorizable.sol +++ b/contracts/asset-proxy/contracts/src/interfaces/IAuthorizable.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. @@ -54,7 +54,7 @@ contract IAuthorizable is uint256 index ) external; - + /// @dev Gets all authorized addresses. /// @return Array of authorized addresses. function getAuthorizedAddresses() diff --git a/contracts/asset-proxy/test/authorizable.ts b/contracts/asset-proxy/test/authorizable.ts index 57cd4abf73..2f93334c6a 100644 --- a/contracts/asset-proxy/test/authorizable.ts +++ b/contracts/asset-proxy/test/authorizable.ts @@ -8,7 +8,7 @@ import { } from '@0x/contracts-test-utils'; import { BlockchainLifecycle } from '@0x/dev-utils'; import { RevertReason } from '@0x/types'; -import { BigNumber, OwnableRevertErrors } from '@0x/utils'; +import { BigNumber } from '@0x/utils'; import * as chai from 'chai'; import * as _ from 'lodash'; @@ -27,9 +27,11 @@ describe('Authorizable', () => { before(async () => { await blockchainLifecycle.startAsync(); }); + after(async () => { await blockchainLifecycle.revertAsync(); }); + before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); [owner, address, notOwner] = _.slice(accounts, 0, 3); @@ -39,18 +41,23 @@ describe('Authorizable', () => { txDefaults, ); }); + beforeEach(async () => { await blockchainLifecycle.startAsync(); }); + afterEach(async () => { await blockchainLifecycle.revertAsync(); }); + describe('addAuthorizedAddress', () => { it('should revert if not called by owner', async () => { - const expectedError = new OwnableRevertErrors.OnlyOwnerError(notOwner, owner); - const tx = authorizable.addAuthorizedAddress.sendTransactionAsync(notOwner, { from: notOwner }); - return expect(tx).to.revertWith(expectedError); + await expectTransactionFailedAsync( + authorizable.addAuthorizedAddress.sendTransactionAsync(notOwner, { from: notOwner }), + RevertReason.OnlyContractOwner, + ); }); + it('should allow owner to add an authorized address', async () => { await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync( address, @@ -60,6 +67,7 @@ describe('Authorizable', () => { const isAuthorized = await authorizable.authorized.callAsync(address); expect(isAuthorized).to.be.true(); }); + it('should revert if owner attempts to authorize a duplicate address', async () => { await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync( address, @@ -80,9 +88,10 @@ describe('Authorizable', () => { { from: owner }, constants.AWAIT_TRANSACTION_MINED_MS, ); - const expectedError = new OwnableRevertErrors.OnlyOwnerError(notOwner, owner); - const tx = authorizable.removeAuthorizedAddress.sendTransactionAsync(address, { from: notOwner }); - return expect(tx).to.revertWith(expectedError); + await expectTransactionFailedAsync( + authorizable.removeAuthorizedAddress.sendTransactionAsync(address, { from: notOwner }), + RevertReason.OnlyContractOwner, + ); }); it('should allow owner to remove an authorized address', async () => { @@ -118,12 +127,14 @@ describe('Authorizable', () => { constants.AWAIT_TRANSACTION_MINED_MS, ); const index = new BigNumber(0); - const expectedError = new OwnableRevertErrors.OnlyOwnerError(notOwner, owner); - const tx = authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, { - from: notOwner, - }); - return expect(tx).to.revertWith(expectedError); + await expectTransactionFailedAsync( + authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, { + from: notOwner, + }), + RevertReason.OnlyContractOwner, + ); }); + it('should revert if index is >= authorities.length', async () => { await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync( address, @@ -138,6 +149,7 @@ describe('Authorizable', () => { RevertReason.IndexOutOfBounds, ); }); + it('should revert if owner attempts to remove an address that is not authorized', async () => { const index = new BigNumber(0); return expectTransactionFailedAsync( @@ -147,6 +159,7 @@ describe('Authorizable', () => { RevertReason.TargetNotAuthorized, ); }); + it('should revert if address at index does not match target', async () => { const address1 = address; const address2 = notOwner; @@ -168,6 +181,7 @@ describe('Authorizable', () => { RevertReason.AuthorizedAddressMismatch, ); }); + it('should allow owner to remove an authorized address', async () => { await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync( address, diff --git a/contracts/utils/CHANGELOG.json b/contracts/utils/CHANGELOG.json index b457e5c0f3..8cf9dbdc51 100644 --- a/contracts/utils/CHANGELOG.json +++ b/contracts/utils/CHANGELOG.json @@ -29,6 +29,10 @@ { "note": "Added unit tests for all of the internal functions in the package", "pr": 2014 + }, + { + "note": "Updated Ownable to revert when the owner attempts to transfer ownership to the zero address", + "pr": 2019 } ] }, diff --git a/contracts/utils/compiler.json b/contracts/utils/compiler.json index c1de9dcb0e..ebd1f5898c 100644 --- a/contracts/utils/compiler.json +++ b/contracts/utils/compiler.json @@ -24,6 +24,7 @@ } }, "contracts": [ + "src/Authorizable.sol", "src/LibAddress.sol", "src/LibBytes.sol", "src/LibEIP1271.sol", @@ -31,6 +32,7 @@ "src/Ownable.sol", "src/ReentrancyGuard.sol", "src/SafeMath.sol", + "src/interfaces/IAuthorizable.sol", "src/interfaces/IOwnable.sol", "test/TestConstants.sol", "test/TestLibAddress.sol", diff --git a/contracts/utils/contracts/src/Authorizable.sol b/contracts/utils/contracts/src/Authorizable.sol new file mode 100644 index 0000000000..cbd8ac475c --- /dev/null +++ b/contracts/utils/contracts/src/Authorizable.sol @@ -0,0 +1,125 @@ +/* + + 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 "./interfaces/IAuthorizable.sol"; +import "./LibAuthorizableRichErrors.sol"; +import "./LibRichErrors.sol"; +import "./Ownable.sol"; + + +contract Authorizable is + Ownable, + IAuthorizable +{ + /// @dev Only authorized addresses can invoke functions with this modifier. + modifier onlyAuthorized { + if (!authorized[msg.sender]) { + LibRichErrors._rrevert(LibAuthorizableRichErrors.SenderNotAuthorizedError(msg.sender)); + } + _; + } + + mapping (address => bool) public authorized; + address[] public authorities; + + /// @dev Authorizes an address. + /// @param target Address to authorize. + function addAuthorizedAddress(address target) + external + onlyOwner + { + // Ensure that the target is not the zero address. + if (target == address(0)) { + LibRichErrors._rrevert(LibAuthorizableRichErrors.ZeroCantBeAuthorizedError()); + } + + // Ensure that the target is not already authorized. + if (authorized[target]) { + LibRichErrors._rrevert(LibAuthorizableRichErrors.TargetAlreadyAuthorizedError(target)); + } + + authorized[target] = true; + authorities.push(target); + emit AuthorizedAddressAdded(target, msg.sender); + } + + /// @dev Removes authorizion of an address. + /// @param target Address to remove authorization from. + function removeAuthorizedAddress(address target) + external + onlyOwner + { + if (!authorized[target]) { + LibRichErrors._rrevert(LibAuthorizableRichErrors.TargetNotAuthorizedError(target)); + } + + delete authorized[target]; + for (uint256 i = 0; i < authorities.length; i++) { + if (authorities[i] == target) { + authorities[i] = authorities[authorities.length - 1]; + authorities.length -= 1; + break; + } + } + emit AuthorizedAddressRemoved(target, msg.sender); + } + + /// @dev Removes authorizion of an address. + /// @param target Address to remove authorization from. + /// @param index Index of target in authorities array. + function removeAuthorizedAddressAtIndex( + address target, + uint256 index + ) + external + onlyOwner + { + if (!authorized[target]) { + LibRichErrors._rrevert(LibAuthorizableRichErrors.TargetNotAuthorizedError(target)); + } + if (index >= authorities.length) { + LibRichErrors._rrevert(LibAuthorizableRichErrors.IndexOutOfBoundsError( + index, + authorities.length + )); + } + if (authorities[index] != target) { + LibRichErrors._rrevert(LibAuthorizableRichErrors.AuthorizedAddressMismatchError( + authorities[index], + target + )); + } + + delete authorized[target]; + authorities[index] = authorities[authorities.length - 1]; + authorities.length -= 1; + emit AuthorizedAddressRemoved(target, msg.sender); + } + + /// @dev Gets all authorized addresses. + /// @return Array of authorized addresses. + function getAuthorizedAddresses() + external + view + returns (address[] memory) + { + return authorities; + } +} diff --git a/contracts/utils/contracts/src/LibAuthorizableRichErrors.sol b/contracts/utils/contracts/src/LibAuthorizableRichErrors.sol new file mode 100644 index 0000000000..5cec572a4e --- /dev/null +++ b/contracts/utils/contracts/src/LibAuthorizableRichErrors.sol @@ -0,0 +1,119 @@ +/* + + 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 LibAuthorizableRichErrors { + + // bytes4(keccak256("AuthorizedAddressMismatchError(address,address)")) + bytes4 internal constant AUTHORIZED_ADDRESS_MISMATCH_ERROR_SELECTOR = + 0x140a84db; + + // bytes4(keccak256("IndexOutOfBoundsError(uint256,uint256)")) + bytes4 internal constant INDEX_OUT_OF_BOUNDS_ERROR_SELECTOR = + 0xe9f83771; + + // bytes4(keccak256("SenderNotAuthorizedError(address)")) + bytes4 internal constant SENDER_NOT_AUTHORIZED_ERROR_SELECTOR = + 0xb65a25b9; + + // bytes4(keccak256("TargetAlreadyAuthorizedError(address)")) + bytes4 internal constant TARGET_ALREADY_AUTHORIZED_ERROR_SELECTOR = + 0xde16f1a0; + + // bytes4(keccak256("TargetNotAuthorizedError(address)")) + bytes4 internal constant TARGET_NOT_AUTHORIZED_ERROR_SELECTOR = + 0xeb5108a2; + + // bytes4(keccak256("ZeroCantBeAuthorizedError()")) + bytes internal constant ZERO_CANT_BE_AUTHORIZED_ERROR_BYTES = + hex"57654fe4"; + + // solhint-disable func-name-mixedcase + function AuthorizedAddressMismatchError( + address authorized, + address target + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + AUTHORIZED_ADDRESS_MISMATCH_ERROR_SELECTOR, + authorized, + target + ); + } + + function IndexOutOfBoundsError( + uint256 index, + uint256 length + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + INDEX_OUT_OF_BOUNDS_ERROR_SELECTOR, + index, + length + ); + } + + function SenderNotAuthorizedError(address sender) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + SENDER_NOT_AUTHORIZED_ERROR_SELECTOR, + sender + ); + } + + function TargetAlreadyAuthorizedError(address target) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + TARGET_ALREADY_AUTHORIZED_ERROR_SELECTOR, + target + ); + } + + function TargetNotAuthorizedError(address target) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + TARGET_NOT_AUTHORIZED_ERROR_SELECTOR, + target + ); + } + + function ZeroCantBeAuthorizedError() + internal + pure + returns (bytes memory) + { + return ZERO_CANT_BE_AUTHORIZED_ERROR_BYTES; + } +} diff --git a/contracts/utils/contracts/src/LibOwnableRichErrors.sol b/contracts/utils/contracts/src/LibOwnableRichErrors.sol index f16c49e9d1..0ea83e2007 100644 --- a/contracts/utils/contracts/src/LibOwnableRichErrors.sol +++ b/contracts/utils/contracts/src/LibOwnableRichErrors.sol @@ -4,9 +4,13 @@ pragma solidity ^0.5.9; library LibOwnableRichErrors { // bytes4(keccak256("OnlyOwnerError(address,address)")) - bytes4 internal constant ONLY_OWNER_SELECTOR = + bytes4 internal constant ONLY_OWNER_ERROR_SELECTOR = 0x1de45ad1; + // bytes4(keccak256("TransferOwnerToZeroError()")) + bytes internal constant TRANSFER_OWNER_TO_ZERO_ERROR_BYTES = + hex"e69edc3e"; + // solhint-disable func-name-mixedcase function OnlyOwnerError( address sender, @@ -17,9 +21,17 @@ library LibOwnableRichErrors { returns (bytes memory) { return abi.encodeWithSelector( - ONLY_OWNER_SELECTOR, + ONLY_OWNER_ERROR_SELECTOR, sender, owner ); } + + function TransferOwnerToZeroError() + internal + pure + returns (bytes memory) + { + return TRANSFER_OWNER_TO_ZERO_ERROR_BYTES; + } } diff --git a/contracts/utils/contracts/src/Ownable.sol b/contracts/utils/contracts/src/Ownable.sol index 36af8f37da..63a10e5473 100644 --- a/contracts/utils/contracts/src/Ownable.sol +++ b/contracts/utils/contracts/src/Ownable.sol @@ -30,7 +30,9 @@ contract Ownable is public onlyOwner { - if (newOwner != address(0)) { + if (newOwner == address(0)) { + LibRichErrors._rrevert(LibOwnableRichErrors.TransferOwnerToZeroError()); + } else { owner = newOwner; } } diff --git a/contracts/utils/contracts/src/interfaces/IAuthorizable.sol b/contracts/utils/contracts/src/interfaces/IAuthorizable.sol new file mode 100644 index 0000000000..cfdaea1439 --- /dev/null +++ b/contracts/utils/contracts/src/interfaces/IAuthorizable.sol @@ -0,0 +1,64 @@ +/* + + 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 "./IOwnable.sol"; + + +contract IAuthorizable is + IOwnable +{ + // Event logged when a new address is authorized. + event AuthorizedAddressAdded( + address indexed target, + address indexed caller + ); + + // Event logged when a currently authorized address is unauthorized. + event AuthorizedAddressRemoved( + address indexed target, + address indexed caller + ); + + /// @dev Authorizes an address. + /// @param target Address to authorize. + function addAuthorizedAddress(address target) + external; + + /// @dev Removes authorizion of an address. + /// @param target Address to remove authorization from. + function removeAuthorizedAddress(address target) + external; + + /// @dev Removes authorizion of an address. + /// @param target Address to remove authorization from. + /// @param index Index of target in authorities array. + function removeAuthorizedAddressAtIndex( + address target, + uint256 index + ) + external; + + /// @dev Gets all authorized addresses. + /// @return Array of authorized addresses. + function getAuthorizedAddresses() + external + view + returns (address[] memory); +} diff --git a/contracts/utils/package.json b/contracts/utils/package.json index 55aa8618ab..a369e1cd41 100644 --- a/contracts/utils/package.json +++ b/contracts/utils/package.json @@ -34,7 +34,7 @@ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" }, "config": { - "abis": "./generated-artifacts/@(IOwnable|LibAddress|LibBytes|LibEIP1271|LibEIP712|Ownable|ReentrancyGuard|SafeMath|TestConstants|TestLibAddress|TestLibAddressArray|TestLibBytes|TestLibEIP712|TestLibRichErrors|TestOwnable|TestReentrancyGuard|TestSafeMath).json", + "abis": "./generated-artifacts/@(Authorizable|IAuthorizable|IOwnable|LibAddress|LibBytes|LibEIP1271|LibEIP712|Ownable|ReentrancyGuard|SafeMath|TestConstants|TestLibAddress|TestLibAddressArray|TestLibBytes|TestLibEIP712|TestLibRichErrors|TestOwnable|TestReentrancyGuard|TestSafeMath).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/utils/src/artifacts.ts b/contracts/utils/src/artifacts.ts index 52d1002075..c9ae81ca55 100644 --- a/contracts/utils/src/artifacts.ts +++ b/contracts/utils/src/artifacts.ts @@ -5,6 +5,8 @@ */ import { ContractArtifact } from 'ethereum-types'; +import * as Authorizable from '../generated-artifacts/Authorizable.json'; +import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json'; import * as IOwnable from '../generated-artifacts/IOwnable.json'; import * as LibAddress from '../generated-artifacts/LibAddress.json'; import * as LibBytes from '../generated-artifacts/LibBytes.json'; @@ -23,6 +25,7 @@ import * as TestOwnable from '../generated-artifacts/TestOwnable.json'; import * as TestReentrancyGuard from '../generated-artifacts/TestReentrancyGuard.json'; import * as TestSafeMath from '../generated-artifacts/TestSafeMath.json'; export const artifacts = { + Authorizable: Authorizable as ContractArtifact, LibAddress: LibAddress as ContractArtifact, LibBytes: LibBytes as ContractArtifact, LibEIP1271: LibEIP1271 as ContractArtifact, @@ -30,6 +33,7 @@ export const artifacts = { Ownable: Ownable as ContractArtifact, ReentrancyGuard: ReentrancyGuard as ContractArtifact, SafeMath: SafeMath as ContractArtifact, + IAuthorizable: IAuthorizable as ContractArtifact, IOwnable: IOwnable as ContractArtifact, TestConstants: TestConstants as ContractArtifact, TestLibAddress: TestLibAddress as ContractArtifact, diff --git a/contracts/utils/src/wrappers.ts b/contracts/utils/src/wrappers.ts index 3caa25b828..daf082ed59 100644 --- a/contracts/utils/src/wrappers.ts +++ b/contracts/utils/src/wrappers.ts @@ -3,6 +3,8 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ +export * from '../generated-wrappers/authorizable'; +export * from '../generated-wrappers/i_authorizable'; export * from '../generated-wrappers/i_ownable'; export * from '../generated-wrappers/lib_address'; export * from '../generated-wrappers/lib_bytes'; diff --git a/contracts/utils/test/authorizable.ts b/contracts/utils/test/authorizable.ts new file mode 100644 index 0000000000..63f99d5031 --- /dev/null +++ b/contracts/utils/test/authorizable.ts @@ -0,0 +1,208 @@ +import { chaiSetup, constants, provider, txDefaults, web3Wrapper } from '@0x/contracts-test-utils'; +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { AuthorizableRevertErrors, BigNumber, OwnableRevertErrors } from '@0x/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { artifacts, AuthorizableContract } from '../src'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('Authorizable', () => { + let owner: string; + let notOwner: string; + let address: string; + let authorizable: AuthorizableContract; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + [owner, address, notOwner] = _.slice(accounts, 0, 3); + authorizable = await AuthorizableContract.deployFrom0xArtifactAsync( + artifacts.Authorizable, + provider, + txDefaults, + ); + }); + + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('addAuthorizedAddress', () => { + it('should revert if not called by owner', async () => { + const expectedError = new OwnableRevertErrors.OnlyOwnerError(notOwner, owner); + const tx = authorizable.addAuthorizedAddress.sendTransactionAsync(notOwner, { from: notOwner }); + return expect(tx).to.revertWith(expectedError); + }); + + it('should allow owner to add an authorized address', async () => { + await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync(address, { from: owner }); + const isAuthorized = await authorizable.authorized.callAsync(address); + expect(isAuthorized).to.be.true(); + }); + + it('should revert if owner attempts to authorize the zero address', async () => { + const expectedError = new AuthorizableRevertErrors.ZeroCantBeAuthorizedError(); + const tx = authorizable.addAuthorizedAddress.sendTransactionAsync(constants.NULL_ADDRESS, { from: owner }); + return expect(tx).to.revertWith(expectedError); + }); + + it('should revert if owner attempts to authorize a duplicate address', async () => { + await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync( + address, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const expectedError = new AuthorizableRevertErrors.TargetAlreadyAuthorizedError(address); + const tx = authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }); + return expect(tx).to.revertWith(expectedError); + }); + }); + + describe('removeAuthorizedAddress', () => { + it('should revert if not called by owner', async () => { + await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync( + address, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const expectedError = new OwnableRevertErrors.OnlyOwnerError(notOwner, owner); + const tx = authorizable.removeAuthorizedAddress.sendTransactionAsync(address, { from: notOwner }); + return expect(tx).to.revertWith(expectedError); + }); + + it('should allow owner to remove an authorized address', async () => { + await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync( + address, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await authorizable.removeAuthorizedAddress.awaitTransactionSuccessAsync( + address, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const isAuthorized = await authorizable.authorized.callAsync(address); + expect(isAuthorized).to.be.false(); + }); + + it('should revert if owner attempts to remove an address that is not authorized', async () => { + const expectedError = new AuthorizableRevertErrors.TargetNotAuthorizedError(address); + const tx = authorizable.removeAuthorizedAddress.sendTransactionAsync(address, { from: owner }); + return expect(tx).to.revertWith(expectedError); + }); + }); + + describe('removeAuthorizedAddressAtIndex', () => { + it('should revert if not called by owner', async () => { + await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync( + address, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const index = new BigNumber(0); + const expectedError = new OwnableRevertErrors.OnlyOwnerError(notOwner, owner); + const tx = authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, { + from: notOwner, + }); + return expect(tx).to.revertWith(expectedError); + }); + + it('should revert if index is >= authorities.length', async () => { + await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync( + address, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const index = new BigNumber(1); + const expectedError = new AuthorizableRevertErrors.IndexOutOfBoundsError(index, constants.ZERO_AMOUNT); + const tx = authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, { + from: owner, + }); + return expect(tx).to.revertWith(expectedError); + }); + + it('should revert if owner attempts to remove an address that is not authorized', async () => { + const index = new BigNumber(0); + const expectedError = new AuthorizableRevertErrors.TargetNotAuthorizedError(address); + const tx = authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, { + from: owner, + }); + return expect(tx).to.revertWith(expectedError); + }); + + it('should revert if address at index does not match target', async () => { + const address1 = address; + const address2 = notOwner; + await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync( + address1, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync( + address2, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const address1Index = new BigNumber(0); + const expectedError = new AuthorizableRevertErrors.AuthorizedAddressMismatchError(address1, address2); + const tx = authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address2, address1Index, { + from: owner, + }); + return expect(tx).to.revertWith(expectedError); + }); + + it('should allow owner to remove an authorized address', async () => { + await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync( + address, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const index = new BigNumber(0); + await authorizable.removeAuthorizedAddressAtIndex.awaitTransactionSuccessAsync( + address, + index, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const isAuthorized = await authorizable.authorized.callAsync(address); + expect(isAuthorized).to.be.false(); + }); + }); + + describe('getAuthorizedAddresses', () => { + it('should return all authorized addresses', async () => { + const initial = await authorizable.getAuthorizedAddresses.callAsync(); + expect(initial).to.have.length(0); + await authorizable.addAuthorizedAddress.awaitTransactionSuccessAsync( + address, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const afterAdd = await authorizable.getAuthorizedAddresses.callAsync(); + expect(afterAdd).to.have.length(1); + expect(afterAdd).to.include(address); + await authorizable.removeAuthorizedAddress.awaitTransactionSuccessAsync( + address, + { from: owner }, + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const afterRemove = await authorizable.getAuthorizedAddresses.callAsync(); + expect(afterRemove).to.have.length(0); + }); + }); +}); diff --git a/contracts/utils/test/ownable.ts b/contracts/utils/test/ownable.ts index e204a48d3e..daeb5e6890 100644 --- a/contracts/utils/test/ownable.ts +++ b/contracts/utils/test/ownable.ts @@ -42,12 +42,10 @@ describe('Ownable', () => { }); describe('transferOwnership', () => { - it('should not transfer ownership if the specified new owner is the zero address', async () => { - expect( - ownable.transferOwnership.sendTransactionAsync(constants.NULL_ADDRESS, { from: owner }), - ).to.be.fulfilled(''); - const updatedOwner = await ownable.owner.callAsync(); - expect(updatedOwner).to.be.eq(owner); + it('should revert if the specified new owner is the zero address', async () => { + const expectedError = new OwnableRevertErrors.TransferOwnerToZeroError(); + const tx = ownable.transferOwnership.sendTransactionAsync(constants.NULL_ADDRESS, { from: owner }); + return expect(tx).to.revertWith(expectedError); }); it('should transfer ownership if the specified new owner is not the zero address', async () => { diff --git a/contracts/utils/tsconfig.json b/contracts/utils/tsconfig.json index 3e74470d85..35e669ea77 100644 --- a/contracts/utils/tsconfig.json +++ b/contracts/utils/tsconfig.json @@ -3,6 +3,8 @@ "compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true }, "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], "files": [ + "generated-artifacts/Authorizable.json", + "generated-artifacts/IAuthorizable.json", "generated-artifacts/IOwnable.json", "generated-artifacts/LibAddress.json", "generated-artifacts/LibBytes.json", diff --git a/packages/utils/src/authorizable_revert_errors.ts b/packages/utils/src/authorizable_revert_errors.ts new file mode 100644 index 0000000000..01a1f547ab --- /dev/null +++ b/packages/utils/src/authorizable_revert_errors.ts @@ -0,0 +1,56 @@ +import { BigNumber } from './configured_bignumber'; +import { RevertError } from './revert_error'; + +// tslint:disable:max-classes-per-file +export class AuthorizedAddressMismatchError extends RevertError { + constructor(authorized?: string, target?: string) { + super('AuthorizedAddressMismatchError', 'AuthorizedAddressMismatchError(address authorized, address target)', { + authorized, + target, + }); + } +} + +export class IndexOutOfBoundsError extends RevertError { + constructor(index?: BigNumber, length?: BigNumber) { + super('IndexOutOfBoundsError', 'IndexOutOfBoundsError(uint256 index, uint256 length)', { index, length }); + } +} + +export class SenderNotAuthorizedError extends RevertError { + constructor(sender?: string) { + super('SenderNotAuthorizedError', 'SenderNotAuthorizedError()', { sender }); + } +} + +export class TargetAlreadyAuthorizedError extends RevertError { + constructor(target?: string) { + super('TargetAlreadyAuthorizedError', 'TargetAlreadyAuthorizedError(address target)', { target }); + } +} + +export class TargetNotAuthorizedError extends RevertError { + constructor(target?: string) { + super('TargetNotAuthorizedError', 'TargetNotAuthorizedError(address target)', { target }); + } +} + +export class ZeroCantBeAuthorizedError extends RevertError { + constructor() { + super('ZeroCantBeAuthorizedError', 'ZeroCantBeAuthorizedError()', {}); + } +} + +const types = [ + AuthorizedAddressMismatchError, + IndexOutOfBoundsError, + SenderNotAuthorizedError, + TargetAlreadyAuthorizedError, + TargetNotAuthorizedError, + ZeroCantBeAuthorizedError, +]; + +// Register the types we've defined. +for (const type of types) { + RevertError.registerType(type); +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index a9c392d577..7ecb00b4f9 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,3 +1,4 @@ +import * as AuthorizableRevertErrors from './authorizable_revert_errors'; import * as LibAddressArrayRevertErrors from './lib_address_array_revert_errors'; import * as LibBytesRevertErrors from './lib_bytes_revert_errors'; import * as OwnableRevertErrors from './ownable_revert_errors'; @@ -32,6 +33,7 @@ export { } from './revert_error'; export { + AuthorizableRevertErrors, LibAddressArrayRevertErrors, LibBytesRevertErrors, OwnableRevertErrors, diff --git a/packages/utils/src/ownable_revert_errors.ts b/packages/utils/src/ownable_revert_errors.ts index 4ad886be1e..14682cce88 100644 --- a/packages/utils/src/ownable_revert_errors.ts +++ b/packages/utils/src/ownable_revert_errors.ts @@ -1,5 +1,6 @@ import { RevertError } from './revert_error'; +// tslint:disable:max-classes-per-file export class OnlyOwnerError extends RevertError { constructor(sender?: string, owner?: string) { super('OnlyOwnerError', 'OnlyOwnerError(address sender, address owner)', { @@ -9,5 +10,15 @@ export class OnlyOwnerError extends RevertError { } } -// Register the OnlyOwnerError type -RevertError.registerType(OnlyOwnerError); +export class TransferOwnerToZeroError extends RevertError { + constructor() { + super('TransferOwnerToZeroError', 'TransferOwnerToZeroError()', {}); + } +} + +const types = [OnlyOwnerError, TransferOwnerToZeroError]; + +// Register the types we've defined. +for (const type of types) { + RevertError.registerType(type); +} diff --git a/packages/utils/src/safe_math_revert_errors.ts b/packages/utils/src/safe_math_revert_errors.ts index 897123f8da..42cbf1e355 100644 --- a/packages/utils/src/safe_math_revert_errors.ts +++ b/packages/utils/src/safe_math_revert_errors.ts @@ -1,8 +1,6 @@ import { BigNumber } from './configured_bignumber'; import { RevertError } from './revert_error'; -// tslint:disable:max-classes-per-file - export enum SafeMathErrorCodes { Uint256AdditionOverflow, Uint256MultiplicationOverflow,