Skip to content

[P1-H01] Require additional data in vote commit hash to prevent duplication of votes#1217

Merged
nicholaspai merged 22 commits intomasterfrom
vote-duplication
Apr 14, 2020
Merged

[P1-H01] Require additional data in vote commit hash to prevent duplication of votes#1217
nicholaspai merged 22 commits intomasterfrom
vote-duplication

Conversation

@nicholaspai
Copy link
Copy Markdown
Member

@nicholaspai nicholaspai commented Apr 10, 2020

Resolves #1216
Resolves #1219

Caller now must include an address, price request timestamp, round ID, and pricefeed identifier with their vote hash. The hash is enforced in revealVote.

This includes fixes for the voting-cli and the voter-dapp as well.

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
@nicholaspai nicholaspai added the audit-fix-phase1 Fixes for Phase 1 (DVM) of the OZ audit label Apr 10, 2020
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
// Committed hash doesn't match revealed price and salt
require(keccak256(abi.encode(price, salt)) == voteSubmission.commit, "Invalid commit hash & salt");
// Committed hash doesn't match revealed voter address, price and salt
require(keccak256(abi.encodePacked(price, salt, msg.sender)) == voteSubmission.commit, "Invalid commit hash, address & salt");
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

abi.encode(...., address) does not return the same hash that web3.utils.soliditySha3(....,address) does, but abi.encodePacked does. I'm not 100% sure why this works, but I believe that if an address is input into abi.encode(), its format needs to be manipulated a bit, including getting rid of the leading 0x...

https://ethereum.stackexchange.com/questions/29139/how-does-solidity-tightly-packed-arguments-work-in-sha256/46161

Copy link
Copy Markdown
Member

@mrice32 mrice32 Apr 10, 2020

Choose a reason for hiding this comment

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

My understanding is that encode and encodePacked differ for types that are less than 32 bytes or dynamically sized. Since an address is less than 32 bytes, I'm guessing it's creating the problem. See here for the solidity docs around this. In the docs for soliditySha3, they say that they "tightly pack" the arguments before hashing. So I think this is actually more correct than it was before.

Now that we're tossing more types in here, we may need to be more careful about how we compute soliditySha3 offchain. web3 attempts to autodetect types, but it's not perfect. Since solidity knows the types, if web3 autodetects incorrectly, it could lead to hash differences due to differences in encoding. To solve this problem, web3 offers a way to provide the types of the arguments passed to soliditySha3 (see examples here). We don't need to do this in this PR, but it's something we may consider doing in the future to make sure these hashes are always computed in exactly the same way.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Right. I'll raise an issue for this so we can do in a subsequent PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just to add one more note on this.

I want to explain my concerns around js autodecting types. If you look at the docs, it autodetects bytes, but not addresses. My guess is that addresses just get encoded the same way as bytes, which probably works fine most of the time. However, ~1 out of every 128 addresses has a 0 for the last byte (or first byte), assuming they're evenly distributed. If the string representation of that address drops the trailing 0, then it might be interpreted as a bytes19 by js. However, solidity would still encode that 0 because it knows an address is 20 bytes. This would lead to a different hash in solidity and js. This could similarly cause problems when hashing the bytes32 representation of our identifiers since there will be padded 0s on the end, which may not show up in the string or may be thrown out by web3. I haven't tested any of this, but these sorts of errors are plausible to the point where I think it might make sense to specify the types on the client side to avoid corrupted hashes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Just to add one more note on this.

I want to explain my concerns around js autodecting types. If you look at the docs, it autodetects bytes, but not addresses. My guess is that addresses just get encoded the same way as bytes, which probably works fine most of the time. However, ~1 out of every 128 addresses has a 0 for the last byte (or first byte), assuming they're evenly distributed. If the string representation of that address drops the trailing 0, then it might be interpreted as a bytes19 by js. However, solidity would still encode that 0 because it knows an address is 20 bytes. This would lead to a different hash in solidity and js. This could similarly cause problems when hashing the bytes32 representation of our identifiers since there will be padded 0s on the end, which may not show up in the string or may be thrown out by web3. I haven't tested any of this, but these sorts of errors are plausible to the point where I think it might make sense to specify the types on the client side to avoid corrupted hashes.

Agreed with this. Will add comments to issue.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

what about type casing the input concatenation of price + salt + voter to a string and then it will always be the same, irrespective of type or how js interprets the leading (or trailing) 0 in the address?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@chrismaree that would work but it would require changing the contract interface. Its much easier IMO to just make the client code specify the types to hash:

old code:

const newHash = web3.utils.soliditySha3(
     newPrice,
     newSalt,
     account1,
     time,
     currentRoundId,
     identifier
);

new code:

    const newHash = web3.utils.soliditySha3(
      { t: "int", v: newPrice },
      { t: "int", v: newSalt },
      { t: "address", v: account1 },
      { t: "uint", v: time },
      { t: "uint", v: currentRoundId },
      { t: "bytes32", v: identifier }
    );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@chrismaree while unlikely, one minor issue with the concatenation approach is that it technically can create hash collisions between different inputs:

price1 = 15
salt1 = 100
price1 + salt1 = 15100
price2 = 1
salt2 = 5100
price2 + salt2 = 15100

I think fixed length inputs are always safest for this reason. We should have the inputs to the hash always include all their bits, even if there are leading or trailing zeros.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

great point @mrice32

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
@mrice32
Copy link
Copy Markdown
Member

mrice32 commented Apr 10, 2020

Follow-up PR will add vote timestamp, price identifier, and round ID to the vote commit.

I know this might make this a huge PR, but can we add them all in one PR? Just so it's easier for OZ to review?

Copy link
Copy Markdown
Member

@mrice32 mrice32 left a comment

Choose a reason for hiding this comment

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

Looks awesome! Thanks for tackling this. Like I mentioned in my above comment, I think we should probably go ahead and implement all the suggested additions to the hash in this PR just so it's easier to review the fix.

// Committed hash doesn't match revealed price and salt
require(keccak256(abi.encode(price, salt)) == voteSubmission.commit, "Invalid commit hash & salt");
// Committed hash doesn't match revealed voter address, price and salt
require(keccak256(abi.encodePacked(price, salt, msg.sender)) == voteSubmission.commit, "Invalid commit hash, address & salt");
Copy link
Copy Markdown
Member

@mrice32 mrice32 Apr 10, 2020

Choose a reason for hiding this comment

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

My understanding is that encode and encodePacked differ for types that are less than 32 bytes or dynamically sized. Since an address is less than 32 bytes, I'm guessing it's creating the problem. See here for the solidity docs around this. In the docs for soliditySha3, they say that they "tightly pack" the arguments before hashing. So I think this is actually more correct than it was before.

Now that we're tossing more types in here, we may need to be more careful about how we compute soliditySha3 offchain. web3 attempts to autodetect types, but it's not perfect. Since solidity knows the types, if web3 autodetects incorrectly, it could lead to hash differences due to differences in encoding. To solve this problem, web3 offers a way to provide the types of the arguments passed to soliditySha3 (see examples here). We don't need to do this in this PR, but it's something we may consider doing in the future to make sure these hashes are always computed in exactly the same way.

@nicholaspai
Copy link
Copy Markdown
Member Author

Follow-up PR will add vote timestamp, price identifier, and round ID to the vote commit.

I know this might make this a huge PR, but can we add them all in one PR? Just so it's easier for OZ to review?

That's exactly why I didn't put them in this PR haha. How about I'll add them after an initial round of reviews

@nicholaspai nicholaspai marked this pull request as draft April 10, 2020 18:06
@mrice32
Copy link
Copy Markdown
Member

mrice32 commented Apr 10, 2020

Follow-up PR will add vote timestamp, price identifier, and round ID to the vote commit.

I know this might make this a huge PR, but can we add them all in one PR? Just so it's easier for OZ to review?

That's exactly why I didn't put them in this PR haha. How about I'll add them after an initial round of reviews

Good idea. Let's do that.

@mrice32
Copy link
Copy Markdown
Member

mrice32 commented Apr 10, 2020

@ptare could you take a pass on this PR?

Comment thread core/test/oracle/Voting.js Outdated
const newPrice = getRandomSignedInt();
const newSalt = getRandomUnsignedInt();
const newHash = web3.utils.soliditySha3(newPrice, newSalt);
const newHash = web3.utils.soliditySha3(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@mrice32 Added an example of overriding soliditySha3 type autodetection, but will hold off on making sweeping changes across repo until another PR

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
@nicholaspai nicholaspai changed the title [P1-H01] Revealing votes requires caller's address to match hashed address [P1-H01] Require additional data in vote commit hash to prevent duplication of votes Apr 12, 2020
Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Copy link
Copy Markdown
Contributor

@ptare ptare left a comment

Choose a reason for hiding this comment

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

Change looks good to me. But wouldn't this change have the same issue as PR #1231 in that it breaks the voter dapp? Is my understanding correct?

Comment thread core/contracts/oracle/implementation/Voting.sol
@nicholaspai
Copy link
Copy Markdown
Member Author

Change looks good to me. But wouldn't this change have the same issue as PR #1231 in that it breaks the voter dapp? Is my understanding correct?

Yes this definitely breaks the voter-dapp, and I've made the neccessary changes in voter-dapp/src/ActiveRequests.

@ptare
Copy link
Copy Markdown
Contributor

ptare commented Apr 13, 2020

Right, but don't we need the old voter-dapp in order to vote on DVM upgrades?

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
@nicholaspai
Copy link
Copy Markdown
Member Author

Right, but don't we need the old voter-dapp in order to vote on DVM upgrades?

Ah yes I see the problem now. Can we create two sites?

@ptare
Copy link
Copy Markdown
Contributor

ptare commented Apr 13, 2020

We'll have to, it looks like, since this is a high(ish) severity issue.

Here's what I recommend:

  1. Remove ActiveRequests.js from this PR
  2. Let's figure out how we want to do two sites or at least freeze the voter dapp
  3. Then we can submit this PR and also [P1-N15] Remove EncryptedStore and emit an extra event instead #1231

@nicholaspai
Copy link
Copy Markdown
Member Author

We'll have to, it looks like, since this is a high(ish) severity issue.

Here's what I recommend:

  1. Remove ActiveRequests.js from this PR
  2. Let's figure out how we want to do two sites or at least freeze the voter dapp
  3. Then we can submit this PR and also [P1-N15] Remove EncryptedStore and emit an extra event instead #1231

Sounds like this and #1231 will be blocked by a PR to come that will freeze the voter-dapp site

@nicholaspai nicholaspai marked this pull request as ready for review April 13, 2020 16:53
Copy link
Copy Markdown
Member

@mrice32 mrice32 left a comment

Choose a reason for hiding this comment

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

Looks great! Two minor comments/nits.

assert(await didContractThrow(voting.revealVote(identifier, time, newPrice, newSalt)));
});

it("Multiple voters", async function() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: why was this test deleted?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I deleted this test because it doesn't really add anything to the previous test. The "Multiple voters" test checks that different voters can't reveal each other's votes, but this is implied from the previous test which checks that the hashed account is correct.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's one other check in that test: that a voter who hasn't committed can't reveal. Are we testing that anywhere else?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Isn't that the same thing as the test above? If a voter has not committed, then their account component of the hash must fail (revealVote checks msg.sender).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see, yeah, agreed.

/**
* @notice Reveal a previously committed vote for `identifier` at `time`.
* @dev The revealed `price` and `salt` must match the latest `hash` that `commitVote()` was called with.
* @dev The revealed `address`, `price` and `salt` must match the latest `hash` that `commitVote()` was called with.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this comment also include roundId, identifier, time, etc? Or are those implied?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yup good catch

Signed-off-by: Nick Pai <npai.nyc@gmail.com>
Copy link
Copy Markdown
Member

@mrice32 mrice32 left a comment

Choose a reason for hiding this comment

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

LGTM!

Copy link
Copy Markdown
Member

@chrismaree chrismaree left a comment

Choose a reason for hiding this comment

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

This looks excellent to me, great work!

assert(await didContractThrow(voting.revealVote(identifier, time, price, newSalt)));

// Can't reveal with the incorrect address.
assert(await didContractThrow(voting.revealVote(identifier, time, newPrice, newSalt, { from: account2 })));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is great. Thanks for increaseing the code coverage here.

Comment thread core/contracts/oracle/implementation/Voting.sol Outdated
Comment thread core/contracts/oracle/implementation/Voting.sol Outdated
Comment thread core/contracts/oracle/implementation/Voting.sol Outdated
for (var i = 0; i < requestNum; i++) {
const salt = getRandomUnsignedInt();
const hash = web3.utils.soliditySha3(price, salt);
const hash = computeVoteHash({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

+1 for being thorough here.

nicholaspai and others added 3 commits April 14, 2020 08:23
Co-Authored-By: Chris Maree <christopher.maree@gmail.com>
Co-Authored-By: Chris Maree <christopher.maree@gmail.com>
Co-Authored-By: Chris Maree <christopher.maree@gmail.com>
@nicholaspai nicholaspai merged commit 50a88a3 into master Apr 14, 2020
@nicholaspai nicholaspai deleted the vote-duplication branch April 14, 2020 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

audit-fix-phase1 Fixes for Phase 1 (DVM) of the OZ audit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Force client scripts hashing data via web3.utils.soliditySha3(...) to specify the type of data Votes can be duplicated

4 participants