From f4709ed1cbf3e59ab4b43907e4cccf378f790015 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 29 Oct 2020 17:47:17 -0400 Subject: [PATCH] EP: Add `LibSignature` library (#21) * `@0x/contracts-zero-ex`: Add `LibSignature` library * `@0x/contracts-zero-ex`: Update package.json scripts Co-authored-by: Lawrence Forman --- contracts/zero-ex/CHANGELOG.json | 4 + .../src/errors/LibSignatureRichErrors.sol | 18 ++- .../src/features/libs/LibSignature.sol | 151 ++++++++++++++++++ .../contracts/test/TestLibSignature.sol | 34 ++++ contracts/zero-ex/package.json | 6 +- contracts/zero-ex/src/index.ts | 4 + contracts/zero-ex/src/revert_errors.ts | 30 ++++ contracts/zero-ex/src/signature_utils.ts | 113 +++++++++++++ contracts/zero-ex/test/artifacts.ts | 4 + contracts/zero-ex/test/lib_signature_test.ts | 98 ++++++++++++ contracts/zero-ex/test/wrappers.ts | 2 + contracts/zero-ex/tsconfig.json | 2 + 12 files changed, 463 insertions(+), 3 deletions(-) create mode 100644 contracts/zero-ex/contracts/src/features/libs/LibSignature.sol create mode 100644 contracts/zero-ex/contracts/test/TestLibSignature.sol create mode 100644 contracts/zero-ex/src/revert_errors.ts create mode 100644 contracts/zero-ex/src/signature_utils.ts create mode 100644 contracts/zero-ex/test/lib_signature_test.ts diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index 9953cf7859..978acbe8aa 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Add support for collecting protocol fees in ETH or WETH", "pr": 2 + }, + { + "note": "Add `LibSignature` library", + "pr": 21 } ] }, diff --git a/contracts/zero-ex/contracts/src/errors/LibSignatureRichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibSignatureRichErrors.sol index 6dcc9e41a4..843a2cb1df 100644 --- a/contracts/zero-ex/contracts/src/errors/LibSignatureRichErrors.sol +++ b/contracts/zero-ex/contracts/src/errors/LibSignatureRichErrors.sol @@ -26,7 +26,8 @@ library LibSignatureRichErrors { INVALID_LENGTH, UNSUPPORTED, ILLEGAL, - WRONG_SIGNER + WRONG_SIGNER, + BAD_SIGNATURE_DATA } // solhint-disable func-name-mixedcase @@ -49,4 +50,19 @@ library LibSignatureRichErrors { signature ); } + + function SignatureValidationError( + SignatureValidationErrorCodes code, + bytes32 hash + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("SignatureValidationError(uint8,bytes32)")), + code, + hash + ); + } } diff --git a/contracts/zero-ex/contracts/src/features/libs/LibSignature.sol b/contracts/zero-ex/contracts/src/features/libs/LibSignature.sol new file mode 100644 index 0000000000..0f9b765714 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/libs/LibSignature.sol @@ -0,0 +1,151 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "../../errors/LibSignatureRichErrors.sol"; + + +/// @dev A library for validating signatures. +library LibSignature { + using LibRichErrorsV06 for bytes; + + // '\x19Ethereum Signed Message:\n32\x00\x00\x00\x00' in a word. + uint256 private constant ETH_SIGN_HASH_PREFIX = + 0x19457468657265756d205369676e6564204d6573736167653a0a333200000000; + /// @dev Exclusive upper limit on ECDSA signatures 'R' values. + /// The valid range is given by fig (282) of the yellow paper. + uint256 private constant ECDSA_SIGNATURE_R_LIMIT = + uint256(0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141); + /// @dev Exclusive upper limit on ECDSA signatures 'S' values. + /// The valid range is given by fig (283) of the yellow paper. + uint256 private constant ECDSA_SIGNATURE_S_LIMIT = ECDSA_SIGNATURE_R_LIMIT / 2 + 1; + + /// @dev Allowed signature types. + enum SignatureType { + ILLEGAL, + INVALID, + EIP712, + ETHSIGN + } + + /// @dev Encoded EC signature. + struct Signature { + // How to validate the signature. + SignatureType signatureType; + // EC Signature data. + uint8 v; + // EC Signature data. + bytes32 r; + // EC Signature data. + bytes32 s; + } + + /// @dev Retrieve the signer of a signature. + /// Throws if the signature can't be validated. + /// @param hash The hash that was signed. + /// @param signature The signature. + /// @return recovered The recovered signer address. + function getSignerOfHash( + bytes32 hash, + Signature memory signature + ) + internal + pure + returns (address recovered) + { + // Ensure this is a signature type that can be validated against a hash. + _validateHashCompatibleSignature(hash, signature); + + if (signature.signatureType == SignatureType.EIP712) { + // Signed using EIP712 + recovered = ecrecover( + hash, + signature.v, + signature.r, + signature.s + ); + } else if (signature.signatureType == SignatureType.ETHSIGN) { + // Signed using `eth_sign` + // Need to hash `hash` with "\x19Ethereum Signed Message:\n32" prefix + // in packed encoding. + bytes32 ethSignHash; + assembly { + // Use scratch space + mstore(0, ETH_SIGN_HASH_PREFIX) // length of 28 bytes + mstore(28, hash) // length of 32 bytes + ethSignHash := keccak256(0, 60) + } + recovered = ecrecover( + ethSignHash, + signature.v, + signature.r, + signature.s + ); + } + // `recovered` can be null if the signature values are out of range. + if (recovered == address(0)) { + LibSignatureRichErrors.SignatureValidationError( + LibSignatureRichErrors.SignatureValidationErrorCodes.BAD_SIGNATURE_DATA, + hash + ).rrevert(); + } + } + + /// @dev Validates that a signature is compatible with a hash signee. + /// @param hash The hash that was signed. + /// @param signature The signature. + function _validateHashCompatibleSignature( + bytes32 hash, + Signature memory signature + ) + private + pure + { + // Ensure the r and s are within malleability limits. + if (uint256(signature.r) >= ECDSA_SIGNATURE_R_LIMIT || + uint256(signature.s) >= ECDSA_SIGNATURE_S_LIMIT) + { + LibSignatureRichErrors.SignatureValidationError( + LibSignatureRichErrors.SignatureValidationErrorCodes.BAD_SIGNATURE_DATA, + hash + ).rrevert(); + } + + // Always illegal signature. + if (signature.signatureType == SignatureType.ILLEGAL) { + LibSignatureRichErrors.SignatureValidationError( + LibSignatureRichErrors.SignatureValidationErrorCodes.ILLEGAL, + hash + ).rrevert(); + } + + // Always invalid. + if (signature.signatureType == SignatureType.INVALID) { + LibSignatureRichErrors.SignatureValidationError( + LibSignatureRichErrors.SignatureValidationErrorCodes.ALWAYS_INVALID, + hash + ).rrevert(); + } + + // Solidity should check that the signature type is within enum range for us + // when abi-decoding. + } +} diff --git a/contracts/zero-ex/contracts/test/TestLibSignature.sol b/contracts/zero-ex/contracts/test/TestLibSignature.sol new file mode 100644 index 0000000000..26c8d12060 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestLibSignature.sol @@ -0,0 +1,34 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "../src/features/libs/LibSignature.sol"; + + +contract TestLibSignature { + + function getSignerOfHash(bytes32 hash, LibSignature.Signature calldata signature) + external + pure + returns (address signer) + { + return LibSignature.getSignerOfHash(hash, signature); + } +} diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 22aaf5e8e7..60357239d9 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -10,8 +10,9 @@ "test": "test" }, "scripts": { - "build": "yarn pre_build && tsc -b", + "build": "yarn pre_build && yarn build:ts", "build:ci": "yarn build", + "build:ts": "tsc -b", "pre_build": "run-s compile contracts:gen generate_contract_wrappers contracts:copy", "test": "yarn run_mocha", "rebuild_and_test": "run-s build test", @@ -41,7 +42,7 @@ "config": { "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FeeCollector|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpender|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinDodo|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestLibTokenSpender|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestProtocolFees|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FeeCollector|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpender|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinDodo|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestLibSignature|TestLibTokenSpender|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestProtocolFees|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" }, "repository": { "type": "git", @@ -76,6 +77,7 @@ }, "dependencies": { "@0x/base-contract": "^6.2.11", + "@0x/order-utils": "^10.4.2", "@0x/subproviders": "^6.1.9", "@0x/types": "^3.3.0", "@0x/typescript-typings": "^5.1.5", diff --git a/contracts/zero-ex/src/index.ts b/contracts/zero-ex/src/index.ts index 6ad89649d3..b055abed6c 100644 --- a/contracts/zero-ex/src/index.ts +++ b/contracts/zero-ex/src/index.ts @@ -32,6 +32,7 @@ export { artifacts } from './artifacts'; export * from './migration'; export * from './nonce_utils'; export * from './signed_call_data'; +export * from './signature_utils'; export { AffiliateFeeTransformerContract, BridgeAdapterContract, @@ -48,3 +49,6 @@ export { WethTransformerContract, ZeroExContract, } from './wrappers'; +export * from './revert_errors'; +export { EIP712TypedData } from '@0x/types'; +export { SupportedProvider } from '@0x/subproviders'; diff --git a/contracts/zero-ex/src/revert_errors.ts b/contracts/zero-ex/src/revert_errors.ts new file mode 100644 index 0000000000..5be6dbff1e --- /dev/null +++ b/contracts/zero-ex/src/revert_errors.ts @@ -0,0 +1,30 @@ +// TODO(dorothy-zbornak): Move these into `@0x/protocol-utils` whenever that +// becomes a thing. +// tslint:disable:max-classes-per-file +import { RevertError } from '@0x/utils'; + +export enum SignatureValidationErrorCodes { + AlwaysInvalid = 0, + InvalidLength = 1, + Unsupported = 2, + Illegal = 3, + WrongSigner = 4, + BadSignatureData = 5, +} + +// tslint:disable:max-classes-per-file +export class SignatureValidationError extends RevertError { + constructor(code?: SignatureValidationErrorCodes, hash?: string) { + super('SignatureValidationError', 'SignatureValidationError(uint8 code, bytes32 hash)', { + code, + hash, + }); + } +} + +const types = [SignatureValidationError]; + +// Register the types we've defined. +for (const type of types) { + RevertError.registerType(type); +} diff --git a/contracts/zero-ex/src/signature_utils.ts b/contracts/zero-ex/src/signature_utils.ts new file mode 100644 index 0000000000..1d5fe3ef30 --- /dev/null +++ b/contracts/zero-ex/src/signature_utils.ts @@ -0,0 +1,113 @@ +import { signatureUtils } from '@0x/order-utils'; +import { SupportedProvider } from '@0x/subproviders'; +import { EIP712TypedData } from '@0x/types'; +import { hexUtils, signTypedDataUtils } from '@0x/utils'; +import * as ethjs from 'ethereumjs-util'; + +/** + * Valid signature types on the Exchange Proxy. + */ +export enum SignatureType { + Illegal = 0, + Invalid = 1, + EIP712 = 2, + EthSign = 3, +} + +/** + * Represents a raw EC signature. + */ +export interface ECSignature { + v: number; + r: string; + s: string; +} + +/** + * A complete signature on the Exchange Proxy. + */ +export interface Signature extends ECSignature { + signatureType: SignatureType; +} + +/** + * Sign a hash with the EthSign signature type on a provider. + */ +export async function ethSignHashFromProviderAsync( + signer: string, + hash: string, + provider: SupportedProvider, +): Promise { + const signatureBytes = await signatureUtils.ecSignHashAsync(provider, hash, signer); + const parsed = parsePackedSignatureBytes(signatureBytes); + assertSignatureType(parsed, SignatureType.EthSign); + return parsed; +} + +/** + * Sign a hash with the EthSign signature type, given a private key. + */ +export function ethSignHashWithKey(hash: string, key: string): Signature { + const ethHash = hexUtils.toHex( + ethjs.sha3(hexUtils.concat(ethjs.toBuffer('\x19Ethereum Signed Message:\n32'), hash)), + ); + return { + ...ecSignHashWithKey(ethHash, key), + signatureType: SignatureType.EthSign, + }; +} + +/** + * Sign a typed data object with the EIP712 signature type, given a private key. + */ +export function eip712SignTypedDataWithKey(typedData: EIP712TypedData, key: string): Signature { + const hash = hexUtils.toHex(signTypedDataUtils.generateTypedDataHash(typedData)); + return { + ...ecSignHashWithKey(hash, key), + signatureType: SignatureType.EIP712, + }; +} + +/** + * Sign an EIP712 hash with the EIP712 signature type, given a private key. + */ +export function eip712SignHashWithKey(hash: string, key: string): Signature { + return { + ...ecSignHashWithKey(hash, key), + signatureType: SignatureType.EIP712, + }; +} + +/** + * Generate the EC signature for a hash given a private key. + */ +export function ecSignHashWithKey(hash: string, key: string): ECSignature { + const { v, r, s } = ethjs.ecsign(ethjs.toBuffer(hash), ethjs.toBuffer(key)); + return { + v, + r: ethjs.bufferToHex(r), + s: ethjs.bufferToHex(s), + }; +} + +function assertSignatureType(signature: Signature, expectedType: SignatureType): void { + if (signature.signatureType !== expectedType) { + throw new Error(`Expected signature type to be ${expectedType} but received ${signature.signatureType}.`); + } +} + +function parsePackedSignatureBytes(signatureBytes: string): Signature { + if (hexUtils.size(signatureBytes) !== 66) { + throw new Error(`Expected packed signatureBytes to be 66 bytes long: ${signatureBytes}`); + } + const typeId = parseInt(signatureBytes.slice(-2), 16) as SignatureType; + if (!Object.values(SignatureType).includes(typeId)) { + throw new Error(`Invalid signatureBytes type ID detected: ${typeId}`); + } + return { + signatureType: typeId, + v: parseInt(signatureBytes.slice(2, 4), 16), + r: hexUtils.slice(signatureBytes, 1, 33), + s: hexUtils.slice(signatureBytes, 33), + }; +} diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index ca262ca93f..448ab82be4 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -51,6 +51,7 @@ import * as LibOwnableStorage from '../test/generated-artifacts/LibOwnableStorag import * as LibProxyRichErrors from '../test/generated-artifacts/LibProxyRichErrors.json'; import * as LibProxyStorage from '../test/generated-artifacts/LibProxyStorage.json'; import * as LibReentrancyGuardStorage from '../test/generated-artifacts/LibReentrancyGuardStorage.json'; +import * as LibSignature from '../test/generated-artifacts/LibSignature.json'; import * as LibSignatureRichErrors from '../test/generated-artifacts/LibSignatureRichErrors.json'; import * as LibSignedCallData from '../test/generated-artifacts/LibSignedCallData.json'; import * as LibSimpleFunctionRegistryRichErrors from '../test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json'; @@ -90,6 +91,7 @@ import * as TestFillQuoteTransformerExchange from '../test/generated-artifacts/T import * as TestFillQuoteTransformerHost from '../test/generated-artifacts/TestFillQuoteTransformerHost.json'; import * as TestFullMigration from '../test/generated-artifacts/TestFullMigration.json'; import * as TestInitialMigration from '../test/generated-artifacts/TestInitialMigration.json'; +import * as TestLibSignature from '../test/generated-artifacts/TestLibSignature.json'; import * as TestLibTokenSpender from '../test/generated-artifacts/TestLibTokenSpender.json'; import * as TestMetaTransactionsTransformERC20Feature from '../test/generated-artifacts/TestMetaTransactionsTransformERC20Feature.json'; import * as TestMigrator from '../test/generated-artifacts/TestMigrator.json'; @@ -153,6 +155,7 @@ export const artifacts = { TokenSpenderFeature: TokenSpenderFeature as ContractArtifact, TransformERC20Feature: TransformERC20Feature as ContractArtifact, UniswapFeature: UniswapFeature as ContractArtifact, + LibSignature: LibSignature as ContractArtifact, LibSignedCallData: LibSignedCallData as ContractArtifact, LibTokenSpender: LibTokenSpender as ContractArtifact, FixinCommon: FixinCommon as ContractArtifact, @@ -208,6 +211,7 @@ export const artifacts = { TestFillQuoteTransformerHost: TestFillQuoteTransformerHost as ContractArtifact, TestFullMigration: TestFullMigration as ContractArtifact, TestInitialMigration: TestInitialMigration as ContractArtifact, + TestLibSignature: TestLibSignature as ContractArtifact, TestLibTokenSpender: TestLibTokenSpender as ContractArtifact, TestMetaTransactionsTransformERC20Feature: TestMetaTransactionsTransformERC20Feature as ContractArtifact, TestMigrator: TestMigrator as ContractArtifact, diff --git a/contracts/zero-ex/test/lib_signature_test.ts b/contracts/zero-ex/test/lib_signature_test.ts new file mode 100644 index 0000000000..f93df73b3f --- /dev/null +++ b/contracts/zero-ex/test/lib_signature_test.ts @@ -0,0 +1,98 @@ +import { blockchainTests, expect } from '@0x/contracts-test-utils'; +import { hexUtils } from '@0x/utils'; +import * as ethjs from 'ethereumjs-util'; + +import { SignatureValidationError, SignatureValidationErrorCodes } from '../src/revert_errors'; +import { eip712SignHashWithKey, ethSignHashWithKey, SignatureType } from '../src/signature_utils'; + +import { artifacts } from './artifacts'; +import { TestLibSignatureContract } from './wrappers'; + +const EMPTY_REVERT = 'reverted with no data'; + +blockchainTests.resets('LibSignature library', env => { + let testLib: TestLibSignatureContract; + let signerKey: string; + let signer: string; + + before(async () => { + signerKey = hexUtils.random(); + signer = ethjs.bufferToHex(ethjs.privateToAddress(ethjs.toBuffer(signerKey))); + testLib = await TestLibSignatureContract.deployFrom0xArtifactAsync( + artifacts.TestLibSignature, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + describe('getSignerOfHash()', () => { + it('can recover the signer of an EIP712 signature', async () => { + const hash = hexUtils.random(); + const sig = eip712SignHashWithKey(hash, signerKey); + const recovered = await testLib.getSignerOfHash(hash, sig).callAsync(); + expect(recovered).to.eq(signer); + }); + + it('can recover the signer of an EthSign signature', async () => { + const hash = hexUtils.random(); + const sig = ethSignHashWithKey(hash, signerKey); + const recovered = await testLib.getSignerOfHash(hash, sig).callAsync(); + expect(recovered).to.eq(signer); + }); + + it('throws if the signature type is out of range', async () => { + const hash = hexUtils.random(); + const badType = (Object.values(SignatureType).slice(-1)[0] as number) + 1; + const sig = { + ...ethSignHashWithKey(hash, signerKey), + signatureType: badType, + }; + return expect(testLib.getSignerOfHash(hash, sig).callAsync()).to.be.rejectedWith(EMPTY_REVERT); + }); + + it('throws if the signature data is malformed', async () => { + const hash = hexUtils.random(); + const sig = { + ...ethSignHashWithKey(hash, signerKey), + v: 1, + }; + return expect(testLib.getSignerOfHash(hash, sig).callAsync()).to.revertWith( + new SignatureValidationError(SignatureValidationErrorCodes.BadSignatureData, hash), + ); + }); + + it('throws if an EC value is out of range', async () => { + const hash = hexUtils.random(); + const sig = { + ...ethSignHashWithKey(hash, signerKey), + r: '0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', + }; + return expect(testLib.getSignerOfHash(hash, sig).callAsync()).to.revertWith( + new SignatureValidationError(SignatureValidationErrorCodes.BadSignatureData, hash), + ); + }); + + it('throws if the type is Illegal', async () => { + const hash = hexUtils.random(); + const sig = { + ...ethSignHashWithKey(hash, signerKey), + signatureType: SignatureType.Illegal, + }; + return expect(testLib.getSignerOfHash(hash, sig).callAsync()).to.revertWith( + new SignatureValidationError(SignatureValidationErrorCodes.Illegal, hash), + ); + }); + + it('throws if the type is Invalid', async () => { + const hash = hexUtils.random(); + const sig = { + ...ethSignHashWithKey(hash, signerKey), + signatureType: SignatureType.Invalid, + }; + return expect(testLib.getSignerOfHash(hash, sig).callAsync()).to.revertWith( + new SignatureValidationError(SignatureValidationErrorCodes.AlwaysInvalid, hash), + ); + }); + }); +}); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index de0fbcf62c..c519b7d982 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -49,6 +49,7 @@ export * from '../test/generated-wrappers/lib_ownable_storage'; export * from '../test/generated-wrappers/lib_proxy_rich_errors'; export * from '../test/generated-wrappers/lib_proxy_storage'; export * from '../test/generated-wrappers/lib_reentrancy_guard_storage'; +export * from '../test/generated-wrappers/lib_signature'; export * from '../test/generated-wrappers/lib_signature_rich_errors'; export * from '../test/generated-wrappers/lib_signed_call_data'; export * from '../test/generated-wrappers/lib_simple_function_registry_rich_errors'; @@ -88,6 +89,7 @@ export * from '../test/generated-wrappers/test_fill_quote_transformer_exchange'; export * from '../test/generated-wrappers/test_fill_quote_transformer_host'; export * from '../test/generated-wrappers/test_full_migration'; export * from '../test/generated-wrappers/test_initial_migration'; +export * from '../test/generated-wrappers/test_lib_signature'; export * from '../test/generated-wrappers/test_lib_token_spender'; export * from '../test/generated-wrappers/test_meta_transactions_transform_erc20_feature'; export * from '../test/generated-wrappers/test_migrator'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 960169dcb4..1b30b3979a 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -73,6 +73,7 @@ "test/generated-artifacts/LibProxyRichErrors.json", "test/generated-artifacts/LibProxyStorage.json", "test/generated-artifacts/LibReentrancyGuardStorage.json", + "test/generated-artifacts/LibSignature.json", "test/generated-artifacts/LibSignatureRichErrors.json", "test/generated-artifacts/LibSignedCallData.json", "test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json", @@ -112,6 +113,7 @@ "test/generated-artifacts/TestFillQuoteTransformerHost.json", "test/generated-artifacts/TestFullMigration.json", "test/generated-artifacts/TestInitialMigration.json", + "test/generated-artifacts/TestLibSignature.json", "test/generated-artifacts/TestLibTokenSpender.json", "test/generated-artifacts/TestMetaTransactionsTransformERC20Feature.json", "test/generated-artifacts/TestMigrator.json",