-
Notifications
You must be signed in to change notification settings - Fork 465
Conversation
27f32f3
to
53c3971
Compare
6391dbe
to
c4efdae
Compare
|
||
// Signed using web3.eth_sign | ||
} else if (stype == SignatureType.Ecrecover) { | ||
require(signature.length == 67); |
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.
Shouldn't this be 66?
@@ -0,0 +1,9 @@ | |||
pragma solidity ^0.4.19; |
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.
Remove _v1
from the filename.
s | ||
order.orderHash ^ 0x1, // Domain separator maker/taker | ||
order.taker, | ||
takerSignature |
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.
- The taker signature should be optional (so if
takerSignature.length
is 0, assume taker ismsg.sender
). - We also need to update a mapping to prevent replay attacks.
- I personally prefer the more generalized approaches described in the ZEIP 18 comments. Maybe since we're pretty undecided on the best approach for this, we just make this a separate function for now?
address signer, | ||
bytes signature) | ||
public view | ||
returns (bool valid) |
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.
Change named return to isValid
.
v, | ||
r, | ||
s | ||
order.orderHash ^ 0x1, // Domain separator maker/taker |
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.
If we have a different message format or add a salt, we can probably remove the separator.
|
||
contract ISigner { | ||
|
||
function validate(bytes32 hash, bytes signature) |
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.
why not rename this isValidSignature
for consistency?
40665cb
to
732a46a
Compare
e8becc2
to
d193b5c
Compare
8ef7087
to
2d186f6
Compare
2d186f6
to
d5cfe98
Compare
// TODO: Domain separation: make hash depend on role. (Taker sig should not be valid as maker sig, etc.) | ||
|
||
require(signature.length >= 1); | ||
SignatureType stype = SignatureType(uint8(signature[0])); |
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.
Nit: can we rename stype
to sigType
? Camel-case and less ambiguous.
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.
@recmo our convention is the thumbs up a comment once it's been addressed.
bytes32 s; | ||
|
||
// Zero is always an invalid signature | ||
if (stype == SignatureType.Invalid) { |
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.
Why is this case necessary?
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 not necessary, but it seemed a good idea from a defense programing perspective. Default values are usually zero. Let's not make them accidentally be some sort of signature.
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.
+1
isValid = false; | ||
return; | ||
|
||
// Implicitely signed by caller |
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.
Typo: Implicitly
return; | ||
|
||
// Signed using web3.eth_sign | ||
} else if (stype == SignatureType.Ecrecover) { |
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.
Super cool that V2 will keep backward compatibility support for EC sigs 🥇
// Anything else is illegal | ||
} else { | ||
revert(); | ||
|
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.
Awkward new line. Can we remove?
private pure | ||
returns (bytes32 result) | ||
{ | ||
// require(b.length >= index + 32); |
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 meant to be commented out still?
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 commented for performance reasons. This check is more costly than the actual function. That should not concert us at the moment though, and it would be better to have more checks while we keep breaking stuff.
d5cfe98
to
fdc6cde
Compare
|
||
// Implicitely signed by caller | ||
} else if (stype == SignatureType.Caller) { | ||
require(signature.length == 1); |
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.
Can you give an example of how this would be used? It seems like msg.sender
will always be order.maker
in this case, which seems a bit strange to me.
bytes32 s; | ||
|
||
// Zero is always an invalid signature | ||
if (stype == SignatureType.Invalid) { |
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.
+1
require(order.takerTokenAmount > 0); | ||
require(takerTokenCancelAmount > 0); | ||
require(isValidSignature( | ||
keccak256(orderSchemaHash, order.orderHash), |
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 seems a bit strange to me that we are always including the orderSchemaHash, even when we aren't using an EIP712 signature. What's the logic for this?
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.
The reasoning is that EIP712 is not a standard for signing a hash, but a standard for generating a hash from structured data. Since none of the other signing methods have requirements on the hashing algorithm, we may as well use EIP712 everywhere.
In fact, we should probably use an EIP712 hash for order.orderHash
.
return; | ||
|
||
// Implicitly signed by caller | ||
} else if (signatureType == SignatureType.Caller) { |
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.
Can you provide an example of how this is used? It seems to me that in the current contracts, msg.sender
will always be order.maker
.
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.
The current PR excludes taker abstraction, so for a successful fillOrder
, we always have msg.sender == order.taker
. When taker abstract is added, this will no longer hold true, and this signature type can be used by either maker or taker if they call fillOrder
.
In the current PR the cancelOrder
function also takes a signature. If maker calls cancelOrder
herself she can set the signature to SignatureType.Caller
.
bytes32 s; | ||
|
||
// Zero is always an invalid signature | ||
if (signatureType == SignatureType.Invalid) { |
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.
When would someone pass in an intentionally invalid signature? Why not just revert if the signature is malformed?
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.
I want to reserve 0x00
as an invalid value, because it is the default value and might show up accidentally.
You are right, it should revert()
instead of throwing, to be consistent with other invalid values.
Would there be value in adding a signature schema that rejects (returns false) always? It's pretty much free to add, does hurt and might be useful somehow?
@@ -113,26 +105,19 @@ contract MixinWrapperFunctions is | |||
/// @param orderAddresses Array of address arrays containing individual order addresses. |
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.
The length of the inputs here are no longer guaranteed to be 484 bytes.
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.
What's the significance of 484? I updated fillOrderNoThrow
to take arbitrary lengths.
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.
Sorry, it wouldn't let me comment on the correct line for some reason. The delegatecall takes the input length, and we're still passing in 484.
/// @param s Array of ECDSA signature s parameters. | ||
function batchFillOrdersNoThrow( | ||
|
||
function fillOrdersUpTo( |
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.
Remove this function, we renamed to marketFillOrders
.
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.
Ignore my previous comment, looks like you accidentally renamed batchFillOrdersNoThrow
to fillOrdersUpTo
.
/// @return Validity of order signature. | ||
function isValidSignature( | ||
bytes32 digest, |
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.
I don't really have a preference for digest
over hash
, but this arg is still named hash
in ISigner
.
@@ -159,7 +165,8 @@ contract MixinExchangeCore is | |||
function cancelOrder( | |||
address[5] orderAddresses, | |||
uint256[6] orderValues, | |||
uint256 takerTokenCancelAmount) | |||
uint256 takerTokenCancelAmount, | |||
bytes signature) |
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.
This signature does not include a nonce, so cancelOrders
with this signature mechanism are prone to replays.
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.
In the current implementation we don't even need a signature here since msg.sender
is required to be the maker.
assembly { | ||
result := mload(add(b, index)) | ||
} | ||
} |
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.
Let's add an explicit return here.
s | ||
); | ||
isValid = signer == recovered; | ||
return; |
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.
Let's change all of these returns to return isValid
for clarity.
@@ -100,29 +96,39 @@ contract MixinExchangeCore is | |||
expirationTimestampInSec: orderValues[4], | |||
orderHash: getOrderHash(orderAddresses, orderValues) | |||
}); | |||
|
|||
// Validate order and maker only if first time seen | |||
if (filled[order.orderHash] == 0 && cancelled[order.orderHash] == 0) { |
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.
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Good question! ecrecover
is 3000 gas + 36 for the hash + couple hundred overhead. State read is 200 gas. Let's ignore (partially) canceled orders for now and look at two scenarios:
- Unfilled order: Read both filled and cancelled, then verify signature ~ 4000 gas.
- Partially filled order: Read filled, short circuit, skip verify ~ 200 gas.
So depending on the ratio partial filled / new orders the expect gas cost is 4000 x + 200 (1-x) =
3800 x + 200.
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 getUnavailableTakerTokenAmount
on line 124(244) and again on line 138. This is of course
redundant, as the state can not change in between.
I added a comment to remind us to optimize this.
// 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 comment
The 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 comment
The 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.
|
||
// 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 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?
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.
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 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.
public | ||
returns (bool success, uint256 takerTokenFilledAmount) | ||
{ | ||
bytes4 FILL_ORDER_FUNCTION_SIGNATURE = bytes4(keccak256("fillOrder(address[5],uint256[6],uint256,uint8,bytes32,bytes32)")); | ||
|
||
// Input size is padded to a 4 + n * 32 byte boundary |
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.
If this is not a WIP any more we need more comments. Much more comments.
I'm unable to read that. Let's reference to some solidity documentation or other sources that can help people understand that. (Talking about the whole function. Let's also have it as a rule to have a very good and verbose explanation before every assembly trick. How much does it save, why is it absolutely neccessary? not against assembly in general, but I want other people to be able to understand that even if they're not so experienced.
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.
Feel free to ignore it for now. It's still a prototype.
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, I'm not happy with the complexity of this assembly at all. It's not even tested and probably contains bugs. It does show that there is a solution though. We can reimplement it using byte[] input
so we can construct the input arguments in Solidity instead of assembly.
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.
Added a // TODO
TODO
|
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.
I kind of like adding more explicit support for 0xProject/ZEIPs#7, since this requires one less external call than SignatureType.Contract. Thoughts on adding SignatureType.Approved?
Also, it occurs to me that we should probably be returning the address of the signer rather than a bool in all cases. With taker abstraction, we don't always know the address of the taker in advance (and therefore can't pass in signer
).
How about this:
In the EIP-7 scenario the maker would call
and the taker would call
If the goal is to create an on-chain order book, you can add a wrapper function that includes order details:
|
Hmm... So far my solution was to add I'm not sure which is more elegant. Can we revisit this discussion when we PR the taker abstraction? |
Re presigning an order, why not just use a mapping of orderHash => signer? I also don't think that presigning should actually require a signature (we can simply calculate the orderHash and use that, since |
I try to keep
This would make it specific to the maker signatures, it would not work for taker signatures and potential other kind of signature uses. I'll open a separate PR for this feature, as it is quite a significant extension. |
// (3): (32 - len(signature)) mod 32 | ||
// (4): 420 + len(signature) + (32 - len(signature)) mod 32 | ||
// | ||
// [1]: https://solidity.readthedocs.io/en/develop/abi-spec.html |
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.
Looks like this is even more complex: http://solidity.readthedocs.io/en/develop/abi-spec.html#use-of-dynamic-types
r = get32(signature, 2); | ||
s = get32(signature, 34); | ||
recovered = ecrecover( | ||
keccak256("\x19Ethereum Signed Message:\n32", hash), |
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.
We are really trying to push the community towards ethereum/EIPs#712
|
||
// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
later
-> latter
Description
bytes
with a one-byte flageth_sign
msg.sender
Motivation and Context
This tries to implement on:
It does not try to implement, but is related to:
How Has This Been Tested?
Types of changes
Checklist: