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

Add gasPrice to 0x transactions #2084

Merged
merged 34 commits into from
Aug 24, 2019
Merged

Add gasPrice to 0x transactions #2084

merged 34 commits into from
Aug 24, 2019

Conversation

abandeali1
Copy link
Member

@abandeali1 abandeali1 commented Aug 21, 2019

Description

Implements part of ZEIP-42.

  • Adds a gasPrice field to 0x transactions. Transactions can now only be executed if the Ethereum tx gasPrice matches that of the 0x tx.
  • Breaks out all of the validation in executeTransaction into an _assertExecutableTransaction function.
  • Adds to and refactors MixinTransaction Unit Tests #2069 to reduce some code duplication.
  • Introduces a new TransactionInvalidContextError that replaces the previous reentrancy error.

Testing instructions

Types of changes

Checklist:

  • Prefix PR title with [WIP] if necessary.
  • Add tests to cover changes as needed.
  • Update documentation as needed.
  • Add new entries to the relevant CHANGELOG.jsons.

@coveralls
Copy link

coveralls commented Aug 21, 2019

Coverage Status

Coverage remained the same at 75.804% when pulling 798fb18 on feat/3.0/transactionGasPrice into 6bb3992 on 3.0.

@abandeali1 abandeali1 force-pushed the feat/3.0/transactionGasPrice branch from bd5adb0 to 928c5ba Compare August 21, 2019 05:16
@abandeali1 abandeali1 changed the title [WIP] Add gasPrice to 0x transactions Add gasPrice to 0x transactions Aug 21, 2019
@abandeali1 abandeali1 marked this pull request as ready for review August 21, 2019 15:50
@abandeali1 abandeali1 requested review from dorothy-zbornak, jalextowle and moodlezoup and removed request for fabioberger August 21, 2019 15:50
Copy link
Contributor

@jalextowle jalextowle left a comment

Choose a reason for hiding this comment

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

Nice job, and you finished this fast! I think there is still uncertainty about how reentrancy should be handled, but I'll approve once we've figured that out.

returnData?: string;
}

async function generateZeroExTransactionAsync(
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a very nice refactor 👍This really improves readability.

const encodedDeadbeef = abiEncoder.encode('0xdeadbeef');
expect(
result[0] ===
'0x0000000000000000000000000000000000000000000000000000000000000020'.concat(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that we could likely make this logic nicer by fixing an issue in the AbiEncoder. I'll create a backlog task for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe refactor out the encode() + hex voodoo and use hexConcat() from @0x/contracts-test-utils: hexConcat( '0x0000000000000000000000000000000000000000000000000000000000000020', encodedDeadBeef)

Copy link
Member Author

Choose a reason for hiding this comment

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

Found a super clean solution to this :)

it('should revert if the current time is past the expiration time', async () => {
const currentTimestamp = await getLatestBlockTimestampAsync();
const transaction = await generateZeroExTransactionAsync({
expirationTimeSeconds: new BigNumber(currentTimestamp).minus(10), // Set the expiration time to before the current timestamp
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about using a expirationOffsetFromCurrentTime parameter instead of this? Then we could remove the call to currentTimestamp in our test cases (making them a bit cleaner), and it would still be very easy to read and understand.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't have a strong preference. We aren't using it a ton right now, so it might not be worth the refactor.

return expect(tx).to.revertWith(expectedError);
});
// FIXME - This should be unskipped when the contracts have been updated to fix this problem.
it.skip('should revert if reentrancy occurs in the middle of an executeTransaction call and msg.sender == signer for both calls', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be unskipped before merging this PR?

Copy link
Member Author

@abandeali1 abandeali1 Aug 22, 2019

Choose a reason for hiding this comment

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

This test will be removed (it should fail).

return expect(tx).to.revertWith(expectedError);
});
// FIXME - This should be unskipped when the contracts have been updated to fix this problem.
it.skip('should revert if reentrancy occurs in the middle of an executeTransaction call and msg.sender == signer and then msg.sender != sender', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto

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 actually desirable? Your offline comment about nested reentrancy makes me wonder whether or not this should actually fail.

Copy link
Member Author

Choose a reason for hiding this comment

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

This test will also be removed (it should fail).

requiredGasPrice
));
}

// Prevent reentrancy
if (currentContextAddress != address(0)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that there is a way for us to support nested executeTransaction calls. If we kept a stack variable called startingContextAddress, we could then replace our erasure of the currentContextAddress with the startingContextAddress. In this way, we'd get the best of both worlds by allowing users to submit metatransactions that submit other metatransactions (relayers could possibly make use of this), but they are always executed in the right context. Thoughts?

const _domain = opts.domain === undefined ? domain : opts.domain;
const expirationTimeSeconds =
opts.expirationTimeSeconds === undefined
? new BigNumber(await getLatestBlockTimestampAsync()).plus(10)
Copy link
Contributor

Choose a reason for hiding this comment

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

If we just set this far enough in the future, this function won't need to be async or do an RPC call.

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess we could just use 0 and MAX_UINT256 for all of these values. I can make that change.

it('should revert if signer != msg.sender and the signature is invalid', async () => {
const transaction = await generateZeroExTransactionAsync({ signerAddress: accounts[0] });
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
const invalidSignature = '0x0000';
Copy link
Contributor

Choose a reason for hiding this comment

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

Might as well pop this bad boy into a constant.

@@ -91,7 +91,7 @@ jobs:
keys:
- repo-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn wsrun test:circleci @0x/contracts-multisig @0x/contracts-utils @0x/contracts-exchange-libs @0x/contracts-erc20 @0x/contracts-erc721 @0x/contracts-erc1155 @0x/contracts-extensions @0x/contracts-asset-proxy @0x/contracts-exchange @0x/contracts-exchange-forwarder @0x/contracts-coordinator @0x/contracts-dev-utils @0x/contracts-staking
test-contracts-ganache-3.0:
test-exchange-ganache-3.0:
Copy link
Contributor

Choose a reason for hiding this comment

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

clever girl
As requested.

const encodedDeadbeef = abiEncoder.encode('0xdeadbeef');
expect(
result[0] ===
'0x0000000000000000000000000000000000000000000000000000000000000020'.concat(
Copy link
Contributor

Choose a reason for hiding this comment

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

Equality should be moved outside into a to.eq(...) so we can see the values if it fails.

const encodedDeadbeef = abiEncoder.encode('0xdeadbeef');
expect(
result[0] ===
'0x0000000000000000000000000000000000000000000000000000000000000020'.concat(
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe refactor out the encode() + hex voodoo and use hexConcat() from @0x/contracts-test-utils: hexConcat( '0x0000000000000000000000000000000000000000000000000000000000000020', encodedDeadBeef)

});
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
// Verify that the returndata of the transaction is 0xDEADBEEF
const result = await transactionsContract.executeTransaction.callAsync(transaction, validSignature, {
Copy link
Contributor

Choose a reason for hiding this comment

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

The TransactionHelper class in @0x/contracts-test-utils might reduce some of the code in blocks where you do a callAsync, sendTransactionAsync, and getTxWithDecodedLogsAsync since it does all three for you.

public
returns (bytes memory)
{
emit ExecutableCalled(data, returnData);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it might be super cool to log the current context address (and assert it), too.

const tx = transactionsContract.executeTransaction.sendTransactionAsync(transaction2, validSignature, {
from: accounts[0],
});
return expect(tx).to.revertWith(expectedError);
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess now we should check that this case doesn't revert, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Do we want to test for that or just remove the test?

expect(logs[3].event).to.be.eq('TransactionExecution');
expect(logs[3].args.transactionHash).to.eq(transactionHash2);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we be testing recursion? Here's a weird edge case that seems like it would pass:

batchExecuteTransactions(
    [ 
        executeTransaction(
            batchExecuteTransaction(
                [
                    executeTransaction(executable(...), from=Bob),
                    executeTransaction(executable(...), from=Charlie),
                ],
                from=Alice,
            ),
            from=Alice,
        )
    ],
    from=Alice,
)

Copy link
Member Author

Choose a reason for hiding this comment

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

This should pass. Why is it an edge case?

Copy link
Contributor

Choose a reason for hiding this comment

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

Because it's an ultra specific scenario that takes advantage of all the reentrancy and context management features in the system. Basically, if we changed any one of them this would probably fail so it might be a good way to check for regressions.

Copy link
Contributor

Choose a reason for hiding this comment

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

@dorothy-zbornak Do you think it would make sense to add a simpler test for this? In this case, we could nest in an equally valid way by excluding the batchExecuteTransaction and just adding a single call to executeTransaction. Not to say that we shouldn't include the test case that you describe, but I think it would also be good to have a super simple case.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, we could probably afford to peel off one layer. I think all the other stuff kind of has to go together to hit all the paths at once, though. 🤔 I do think we're pretty well covered for now.

const transaction1 = await generateZeroExTransactionAsync({ signerAddress: accounts[0] });
const transactionHash1 = transactionHashUtils.getTransactionHashHex(transaction1);
const transaction2 = await generateZeroExTransactionAsync({
signerAddress: accounts[0],
Copy link
Contributor

Choose a reason for hiding this comment

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

Wait, does this align with what the description says? Shouldn't it be accounts[1] here?

Also, we should have a test that's similar but where the nested tx doesn't have a valid signature, just to assert that we are checking signatures on inner transactions.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that this is correct. The transaction is sent with accounts[0], which is the signerAddress for the outerTransaction. The signer of the inner transaction is accounts[1], so it seems like the description is correct. Not sure if this comment still applies after the changes though.

Copy link
Contributor

@moodlezoup moodlezoup left a comment

Choose a reason for hiding this comment

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

A few nits but otherwise everything has either already been covered or lgtm

@@ -125,6 +125,10 @@ library LibExchangeRichErrors {
// bytes4(keccak256("TransactionExecutionError(bytes32,bytes)"))
bytes4 internal constant TRANSACTION_EXECUTION_ERROR_SELECTOR =
0x20d11f61;

// bytes4(keccak256("TransactionGasPriceError(bytes32,unt256,uint256)"))
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: uint256

currentContextAddress = context;
}

function setTransactionHash(bytes32 hash)
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe rename to setTransactionExecuted

@@ -207,6 +207,20 @@ export class TransactionExecutionError extends RevertError {
}
}

export class TransactionGasPriceError extends RevertError {
constructor(transactionHash?: string, actualGasPrice?: BigNumber, requiredGasPrice?: BigNumber) {
Copy link
Contributor

Choose a reason for hiding this comment

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

elsewhere we type uint256 parameters as BigNumber | number | string, we should stick to one convention or the other

Copy link
Member Author

Choose a reason for hiding this comment

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

We just use BigNumber in more of the constructors than not, so I will remove the number | string in any other constructors.

),
).to.be.true();

// Ensure that the result contains the abi-encoded bytes "0xdeadbeef"
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: 0xbeefdead


// Ensure that the result contains the abi-encoded bytes "0xdeadbeef"
const encodedBeefdead = abiEncoder.encode('0xbeefdead');
expect(result.length).to.be.eq(2);
Copy link
Contributor

Choose a reason for hiding this comment

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

already checked above

@abandeali1 abandeali1 force-pushed the feat/3.0/transactionGasPrice branch from b26d470 to 5986577 Compare August 23, 2019 00:10
@abandeali1 abandeali1 force-pushed the feat/3.0/transactionGasPrice branch from 5986577 to 5e51233 Compare August 23, 2019 00:12
Copy link
Contributor

@dorothy-zbornak dorothy-zbornak left a comment

Choose a reason for hiding this comment

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

One-ish change then g2g! Well done!

);
expect(transactionsContract.executeTransaction.getABIDecodedReturnData(result[1])).to.equal(returnData2);
Copy link
Contributor

Choose a reason for hiding this comment

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

aha! 🎉

accounts[0],
).encode();
const expectedError = new ExchangeRevertErrors.TransactionExecutionError(outerTransactionHash, errorData);
const tx = transactionsContract.executeTransaction.sendTransactionAsync(outerTransaction, validSignature, {
Copy link
Contributor

Choose a reason for hiding this comment

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

These should all be awaitTransactionSuccessAsync() calls, even if they revert.

Copy link
Contributor

@jalextowle jalextowle left a comment

Choose a reason for hiding this comment

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

Good stuff (ง ͡ʘ ͜ʖ ͡ʘ)ง

Looks ready to merge after thinking a bit about the comments on test cases. At one point, I recommend removing a test case, which makes me think that at some point it might be a good idea to generalize ReentrancyTester to check for statements like assertExecutableTransaction in functions rather than using redundant test cases. Definitely out of scope for this PR, but it's something to think about long term.

const transaction1 = await generateZeroExTransactionAsync({ signerAddress: accounts[0] });
const transactionHash1 = transactionHashUtils.getTransactionHashHex(transaction1);
const transaction2 = await generateZeroExTransactionAsync({
signerAddress: accounts[0],
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that this is correct. The transaction is sent with accounts[0], which is the signerAddress for the outerTransaction. The signer of the inner transaction is accounts[1], so it seems like the description is correct. Not sure if this comment still applies after the changes though.

{ from: accounts[0] },
);

expect(transactionsContract.executeTransaction.getABIDecodedReturnData(result)).to.equal(
Copy link
Contributor

Choose a reason for hiding this comment

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

🎉

}),
).to.revertWith(expectedError);
});
it('should be successful if signer == msg.sender and the signature is invalid', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that we should also check that the tx should be successful if signer == msg.sender and the signature is valid. It's a bit pedantic, but it would make the suite more complete.

expect(logs[3].event).to.be.eq('TransactionExecution');
expect(logs[3].args.transactionHash).to.eq(transactionHash2);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

@dorothy-zbornak Do you think it would make sense to add a simpler test for this? In this case, we could nest in an equally valid way by excluding the batchExecuteTransaction and just adding a single call to executeTransaction. Not to say that we shouldn't include the test case that you describe, but I think it would also be good to have a super simple case.

);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if the transaction is submitted with a gasPrice that does not equal the required gasPrice', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since you already test this behavior in assertExecutableTransaction's test, I'd be fine if we want to remove this. Any reason why we shouldn't?

Copy link
Member Author

Choose a reason for hiding this comment

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

I feel like most of the other tests are actually covered by the assertExecutableTransaction tests. We have extra tests for expired transactions, valid/invalid signatures, already executed transactions, etc. If we remove this, it probably makes sense to remove the other redundant tests as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess out of these options, it might make sense to just keep them to prevent regressions. Longer term, I do think we should try to reduce test duplication, but it's easy to understand which are duplicates in this file.

@abandeali1 abandeali1 force-pushed the feat/3.0/transactionGasPrice branch from 106b171 to 798fb18 Compare August 23, 2019 22:14
@abandeali1 abandeali1 merged commit 8ef0a59 into 3.0 Aug 24, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants