-
Notifications
You must be signed in to change notification settings - Fork 465
Signature generalisation #376
Changes from 25 commits
1f03bb7
2d8b77a
731d71e
d99a8d2
69484dd
46b4ceb
0fd2ded
c5d5c10
a348438
2142890
5faeb90
3a3610b
cdf7c4f
596bd4f
6d4d9cb
3f39bc4
a9c810d
3f130d7
0c4b952
b5a0c7d
a8cec97
43e5bd2
753f9b3
0d9a464
e5e6e8c
095388f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
{ | ||
Order memory order = Order({ | ||
maker: orderAddresses[0], | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have any data on how expensive is the signature validation? Two state reads also consume some gas. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An ecrecover is at least 3000 gas (and your gas profiler actually showed it being one of the most expensive parts of the function). An sload is only 200 I believe. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question!
So depending on the ratio partial filled / new orders the expect gas cost is 4000 x + 200 (1-x) = Status quo is a fixed cost of about 3500. Setting these equal and solving for x gives 87%. So if less than 87% of the fill orders are new orders, this method saves gas. @tomhschmidt : Do we have empirical data on this? But!We need to optimize for the case where the order is valid. In this case, we would need to read both state variables anyway to process the order, so reading it earlier should not add gas cost. Making the optimization essentially free. At the moment we don't optimize for this, and we read the same state again in I added a comment to remind us to optimize this. |
||
require(order.makerTokenAmount > 0); | ||
require(order.takerTokenAmount > 0); | ||
require(isValidSignature( | ||
keccak256(orderSchemaHash, order.orderHash), | ||
order.maker, | ||
signature | ||
)); | ||
} | ||
|
||
// Validate taker | ||
if (order.taker != address(0)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there an efficiency difference between this if block vs There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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; | ||
|
@@ -177,8 +184,10 @@ contract MixinExchangeCore is | |
orderHash: getOrderHash(orderAddresses, orderValues) | ||
}); | ||
|
||
require(order.makerTokenAmount > 0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used to do If there is no performance difference, I prefer not using |
||
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); | ||
|
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All those ifs add to understanding but increase the gas consumption. Will we comment them out in a production version? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was not planning on that, and we need some way to distinguish the cases. What do you suggest instead? A simple trick is to sort the branches by popularity. |
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you provide an example of how this is used? It seems to me that in the current contracts, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current PR excludes taker abstraction, so for a successful In the current PR the |
||
require(signature.length == 1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you give an example of how this would be used? It seems like |
||
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
keccak256("\x19Ethereum Signed Message:\n32", hash), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I need to side with the Trezor guys here. The way There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to assert the signature length? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes we do! |
||
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 later is not | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
// 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isValidSignature seems too generic. Can we change to There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add an explicit return here. |
||
|
||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.