-
Notifications
You must be signed in to change notification settings - Fork 465
Conversation
bd5adb0
to
928c5ba
Compare
gasPrice
to 0x transactionsgasPrice
to 0x transactions
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.
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( |
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 is a very nice refactor 👍This really improves readability.
const encodedDeadbeef = abiEncoder.encode('0xdeadbeef'); | ||
expect( | ||
result[0] === | ||
'0x0000000000000000000000000000000000000000000000000000000000000020'.concat( |
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 that we could likely make this logic nicer by fixing an issue in the AbiEncoder
. I'll create a backlog task 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.
Maybe refactor out the encode()
+ hex voodoo and use hexConcat()
from @0x/contracts-test-utils
: hexConcat( '0x0000000000000000000000000000000000000000000000000000000000000020', encodedDeadBeef)
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.
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 |
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 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.
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 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 () => { |
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 this be unskipped before merging this PR?
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 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 () => { |
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.
Ditto
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 actually desirable? Your offline comment about nested reentrancy makes me wonder whether or not this should actually fail.
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 test will also be removed (it should fail).
requiredGasPrice | ||
)); | ||
} | ||
|
||
// Prevent reentrancy | ||
if (currentContextAddress != address(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.
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) |
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 just set this far enough in the future, this function won't need to be async
or do an RPC call.
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 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'; |
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.
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: |
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.
const encodedDeadbeef = abiEncoder.encode('0xdeadbeef'); | ||
expect( | ||
result[0] === | ||
'0x0000000000000000000000000000000000000000000000000000000000000020'.concat( |
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.
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( |
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.
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, { |
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 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); |
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 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); |
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 guess now we should check that this case doesn't revert, right?
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 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); | ||
}); | ||
}); |
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.
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,
)
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 should pass. Why is it an edge case?
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.
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.
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.
@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.
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, 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], |
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.
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.
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 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.
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.
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)")) |
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: uint256
currentContextAddress = context; | ||
} | ||
|
||
function setTransactionHash(bytes32 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.
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) { |
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.
elsewhere we type uint256 parameters as BigNumber | number | string
, we should stick to one convention or the other
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 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" |
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: 0xbeefdead
|
||
// Ensure that the result contains the abi-encoded bytes "0xdeadbeef" | ||
const encodedBeefdead = abiEncoder.encode('0xbeefdead'); | ||
expect(result.length).to.be.eq(2); |
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.
already checked above
…on to not require state
…hauling transaction_unit_tests.ts
…d the `Killed` error
b26d470
to
5986577
Compare
5986577
to
5e51233
Compare
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.
One-ish change then g2g! Well done!
); | ||
expect(transactionsContract.executeTransaction.getABIDecodedReturnData(result[1])).to.equal(returnData2); |
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.
aha! 🎉
accounts[0], | ||
).encode(); | ||
const expectedError = new ExchangeRevertErrors.TransactionExecutionError(outerTransactionHash, errorData); | ||
const tx = transactionsContract.executeTransaction.sendTransactionAsync(outerTransaction, validSignature, { |
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.
These should all be awaitTransactionSuccessAsync()
calls, even if they 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.
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], |
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 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( |
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.
🎉
}), | ||
).to.revertWith(expectedError); | ||
}); | ||
it('should be successful if signer == msg.sender and the signature is invalid', async () => { |
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 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); | ||
}); | ||
}); |
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.
@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 () => { |
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.
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?
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 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.
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 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.
106b171
to
798fb18
Compare
Description
Implements part of ZEIP-42.
gasPrice
field to 0x transactions. Transactions can now only be executed if the Ethereum txgasPrice
matches that of the 0x tx.executeTransaction
into an_assertExecutableTransaction
function.TransactionInvalidContextError
that replaces the previous reentrancy error.Testing instructions
Types of changes
Checklist:
[WIP]
if necessary.