Skip to content
This repository has been archived by the owner on Jul 9, 2021. It is now read-only.

Signature generalisation #376

Merged
merged 26 commits into from
Mar 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1f03bb7
Abstract signature to opaque bytearray
recmo Feb 10, 2018
2d8b77a
Add 'caller' signature type
recmo Feb 7, 2018
731d71e
Revert after cascade
recmo Feb 14, 2018
d99a8d2
Add external contract call signatures (relates to ZEIP 1 and 7)
recmo Feb 14, 2018
69484dd
Add EIP712 signatures (implements ZEIP-17)
recmo Feb 7, 2018
46b4ceb
Cache order maker signature (ZEIP-15)
recmo Feb 9, 2018
0fd2ded
Cancel signature abstraction
recmo Feb 7, 2018
c5d5c10
Implement EIP712 at verify-signature call site
recmo Feb 13, 2018
a348438
Make first value illegal
recmo Feb 21, 2018
2142890
Add SignatureType.Invalid and documentation
recmo Feb 22, 2018
5faeb90
Fix input length in fillOrderNoThrow
recmo Feb 22, 2018
3a3610b
Fix batchFillOrdersNoThrow name
recmo Feb 22, 2018
cdf7c4f
Fix hash argument name
recmo Feb 22, 2018
596bd4f
Revert cancel order signature abstraction
recmo Feb 22, 2018
6d4d9cb
Explicit returns
recmo Feb 22, 2018
3f39bc4
Make wrappers external again
recmo Feb 22, 2018
a9c810d
Add documentation
recmo Feb 23, 2018
3f130d7
Spelling fixes
recmo Feb 23, 2018
0c4b952
Remove unused return value
recmo Feb 23, 2018
b5a0c7d
Verify length on EIP712 signatures
recmo Feb 23, 2018
a8cec97
Fix documentation
recmo Feb 23, 2018
43e5bd2
Add Todos
recmo Feb 23, 2018
753f9b3
Add Trezor signatures
recmo Feb 24, 2018
0d9a464
Document noThrow wrapper and correct fixed array offset
recmo Feb 26, 2018
e5e6e8c
Add offset to signature
recmo Feb 26, 2018
095388f
Fix spelling of latter
recmo Feb 27, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@
*/

pragma solidity ^0.4.19;
pragma experimental ABIEncoderV2;

import "./MixinExchangeCore.sol";
import "./MixinSignatureValidatorEcrecover.sol";
import "./MixinSignatureValidator.sol";
import "./MixinSettlementProxy.sol";
import "./MixinWrapperFunctions.sol";

contract Exchange is
MixinExchangeCore,
MixinSignatureValidatorEcrecover,
MixinSignatureValidator,
MixinSettlementProxy,
MixinWrapperFunctions
{
Expand All @@ -37,7 +38,7 @@ contract Exchange is
)
public
MixinExchangeCore()
MixinSignatureValidatorEcrecover()
MixinSignatureValidator()
MixinSettlementProxy(_tokenTransferProxy, _zrxToken)
MixinWrapperFunctions()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,11 @@

pragma solidity ^0.4.19;

import "./mixins/MSignatureValidator.sol";

/// @dev Provides MSignatureValidator
contract MixinSignatureValidatorEcrecover is
MSignatureValidator
{
contract ISigner {

function isValidSignature(
address signer,
bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s)
public
constant
returns (bool isValid)
{
isValid = signer == ecrecover(
keccak256("\x19Ethereum Signed Message:\n32", hash),
v,
r,
s
);
return isValid;
}
bytes signature)
public view
returns (bool isValid);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,22 @@
pragma solidity ^0.4.19;

contract LibOrder {


bytes32 constant orderSchemaHash = keccak256(
"address exchangeContractAddress",
"address makerAddress",
"address takerAddress",
"address makerTokenAddress",
"address takerTokenAddress",
"address feeRecipientAddress",
"uint256 makerTokenAmount",
"uint256 takerTokenAmount",
"uint256 makerFeeAmount",
"uint256 takerFeeAmount",
"uint256 expirationTimestamp",
"uint256 salt"
);

struct Order {
address maker;
address taker;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,15 @@ contract MixinExchangeCore is
/// @param orderAddresses Array of order's maker, taker, makerToken, takerToken, and feeRecipient.
/// @param orderValues Array of order's makerTokenAmount, takerTokenAmount, makerFee, takerFee, expirationTimestampInSec, and salt.
/// @param takerTokenFillAmount Desired amount of takerToken to fill.
/// @param v ECDSA signature parameter v.
/// @param r ECDSA signature parameters r.
/// @param s ECDSA signature parameters s.
/// @param signature Proof of signing order by maker.
/// @return Total amount of takerToken filled in trade.
function fillOrder(
address[5] orderAddresses,
uint256[6] orderValues,
uint256 takerTokenFillAmount,
uint8 v,
bytes32 r,
bytes32 s)
public
returns (uint256 takerTokenFilledAmount)
address[5] orderAddresses,
uint[6] orderValues,
uint takerTokenFillAmount,
bytes signature)
public
returns (uint256 takerTokenFilledAmount)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the correct indentation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there was extra indentation before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the convention I'm used to and also matches with what I see in the tokenRegistery contract.

{
Order memory order = Order({
maker: orderAddresses[0],
Expand All @@ -100,29 +96,40 @@ contract MixinExchangeCore is
expirationTimestampInSec: orderValues[4],
orderHash: getOrderHash(orderAddresses, orderValues)
});

// Validate order and maker only if first time seen
// TODO: Read filled and cancelled only once
if (filled[order.orderHash] == 0 && cancelled[order.orderHash] == 0) {
require(order.makerTokenAmount > 0);
require(order.takerTokenAmount > 0);
require(isValidSignature(
keccak256(orderSchemaHash, order.orderHash),
order.maker,
signature
));
}

// Validate taker
if (order.taker != address(0)) {
Copy link
Member

@abandeali1 abandeali1 Feb 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an efficiency difference between this if block vs require(order.taker == address(0) || order.taker == msg.sender)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I know of. It is like this mostly because I reverted the taker abstraction. With taker abstraction we need a more complex expression than order.taker == msg.sender.

require(order.taker == msg.sender);
}
require(takerTokenFillAmount > 0);

require(order.taker == address(0) || order.taker == msg.sender);
require(order.makerTokenAmount > 0 && order.takerTokenAmount > 0 && takerTokenFillAmount > 0);
require(isValidSignature(
order.maker,
order.orderHash,
v,
r,
s
));

// Validate order expiration
if (block.timestamp >= order.expirationTimestampInSec) {
LogError(uint8(Errors.ORDER_EXPIRED), order.orderHash);
return 0;
}


// Validate order availability
uint256 remainingTakerTokenAmount = safeSub(order.takerTokenAmount, getUnavailableTakerTokenAmount(order.orderHash));
takerTokenFilledAmount = min256(takerTokenFillAmount, remainingTakerTokenAmount);
if (takerTokenFilledAmount == 0) {
LogError(uint8(Errors.ORDER_FULLY_FILLED_OR_CANCELLED), order.orderHash);
return 0;
}


// Validate fill order rounding
if (isRoundingError(takerTokenFilledAmount, order.takerTokenAmount, order.makerTokenAmount)) {
LogError(uint8(Errors.ROUNDING_ERROR_TOO_LARGE), order.orderHash);
return 0;
Expand Down Expand Up @@ -177,8 +184,10 @@ contract MixinExchangeCore is
orderHash: getOrderHash(orderAddresses, orderValues)
});

require(order.makerTokenAmount > 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used to do assert(someThing && somethingElse && somethingOther) a lot in a past project. Then one day we get a stack trace from a customer, and it says basically: "assertion failed on line ...". We wasted a lot of time trying to figure out which of the clauses failed and learned a hard lesson on never using && in an assertion :)

If there is no performance difference, I prefer not using && in assertions. Imho it makes it more readable and debuggable.

require(order.takerTokenAmount > 0);
require(takerTokenCancelAmount > 0);
require(order.maker == msg.sender);
require(order.makerTokenAmount > 0 && order.takerTokenAmount > 0 && takerTokenCancelAmount > 0);

if (block.timestamp >= order.expirationTimestampInSec) {
LogError(uint8(Errors.ORDER_EXPIRED), order.orderHash);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*

Copyright 2017 ZeroEx Intl.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

pragma solidity ^0.4.19;

import "./mixins/MSignatureValidator.sol";
import "./ISigner.sol";

/// @dev Provides MSignatureValidator
contract MixinSignatureValidator is
MSignatureValidator
{
enum SignatureType {
Illegal, // Default value
Invalid,
Caller,
Ecrecover,
EIP712,
Trezor,
Contract
}

function isValidSignature(
bytes32 hash,
address signer,
bytes signature)
public view
returns (bool isValid)
{
// TODO: Domain separation: make hash depend on role. (Taker sig should not be valid as maker sig, etc.)

require(signature.length >= 1);
SignatureType signatureType = SignatureType(uint8(signature[0]));

// Variables are not scoped in Solidity
uint8 v;
bytes32 r;
bytes32 s;
address recovered;

// Always illegal signature
// This is always an implicit option since a signer can create a
// signature array with invalid type or length. We may as well make
// it an explicit option. This aids testing and analysis. It is
// also the initialization value for the enum type.
if (signatureType == SignatureType.Illegal) {
revert();

// Always invalid signature
// Like Illegal, this is always implicitly available and therefore
// offered explicitly. It can be implicitly created by providing
// a correctly formatted but incorrect signature.
} else if (signatureType == SignatureType.Invalid) {
require(signature.length == 1);
isValid = false;
return isValid;

// Implicitly signed by caller
// The signer has initiated the call. In the case of non-contract
// accounts it means the transaction itself was signed.
// Example: let's say for a particular operation three signatures
// A, B and C are required. To submit the transaction, A and B can
// give a signature to C, who can then submit the transaction using
// `Caller` for his own signature. Or A and C can sign and B can
// submit using `Caller`. Having `Caller` allows this flexibility.
} else if (signatureType == SignatureType.Caller) {
require(signature.length == 1);
isValid = signer == msg.sender;
return isValid;

// Signed using web3.eth_sign
} else if (signatureType == SignatureType.Ecrecover) {
require(signature.length == 66);
v = uint8(signature[1]);
r = get32(signature, 2);
s = get32(signature, 34);
recovered = ecrecover(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

address recovered

keccak256("\x19Ethereum Signed Message:\n32", hash),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add support for current Trezors which do not use ASCII characters for the message length encoding: ethereum/go-ethereum#14794.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm glad you're adding support for this, but can't give a thumbs up because it's obnoxious it's different in the first place.

Copy link
Contributor Author

@recmo recmo Feb 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I need to side with the Trezor guys here. The way eth_sign does it is a security hole. It allows two different messages to have the same hash. I don't like their solution though, I would rather see some thing more standard like RLP encoding or ABI encoding.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, agreed. varInt is much better than an ASCII representation. It's just so strange there doesn't appear to be an EIP around it and everyone (other than Trezor) just supports both ways like it's no big deal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are really trying to push the community towards ethereum/EIPs#712

v,
r,
s
);
isValid = signer == recovered;
return isValid;

// Signature using EIP712
} else if (signatureType == SignatureType.EIP712) {
require(signature.length == 66);
v = uint8(signature[1]);
r = get32(signature, 2);
s = get32(signature, 34);
recovered = ecrecover(hash, v, r, s);
isValid = signer == recovered;
return isValid;

// Signature from Trezor hardware wallet
// It differs from web3.eth_sign in the encoding of message length
// (Bitcoin varint encoding vs ascii-decimal, the latter is not
// self-terminating which leads to ambiguities).
// See also:
// https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
// https://github.com/trezor/trezor-mcu/blob/master/firmware/ethereum.c#L602
// https://github.com/trezor/trezor-mcu/blob/master/firmware/crypto.c#L36
} else if (signatureType == SignatureType.Trezor) {
require(signature.length == 66);
v = uint8(signature[1]);
r = get32(signature, 2);
s = get32(signature, 34);
recovered = ecrecover(
keccak256("\x19Ethereum Signed Message:\n\x41", hash),
v,
r,
s
);
isValid = signer == recovered;
return isValid;

// Signature verified by signer contract
} else if (signatureType == SignatureType.Contract) {
isValid = ISigner(signer).isValidSignature(hash, signature);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isValidSignature seems too generic. Can we change to isValid0xOrderSignature or smth longer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's intended to be generic though. I could see people using the same function for other purposes as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it is, it is pretty generic. Right now we only use it for maker signature, we will also use it for taker signature and maybe controller signature, etc. These would then sign different objects, not Orders's but FillOrder's and possibly other structures.

return isValid;
}

// Anything else is illegal (We do not return false because
// the signature may actually be valid, just not in a format
// that we currently support. In this case returning false
// may lead the caller to incorrectly believe that the
// signature was invalid.)
revert();
}

function get32(bytes b, uint256 index)
private pure
returns (bytes32 result)
{
require(b.length >= index + 32);

// Arrays are prefixed by a 256 bit length parameter
index += 32;

// Read the bytes32 from array memory
assembly {
result := mload(add(b, index))
}
return result;
}

}
Loading