-
Notifications
You must be signed in to change notification settings - Fork 465
[WIP] Rich Revert Reasons PoC (Exchange only for now) #1688
[WIP] Rich Revert Reasons PoC (Exchange only for now) #1688
Conversation
…et@^1.0.26. Update order-watcher's websocket dependency from ^1.0.25 (broken on some node architectures) to ^1.0.26.
Update order-utils changelog and manifest. Revert metacoin to development (again).
Modify contract method template to attach returnData field to decoding errors.
currentAssetProxy == address(0), | ||
"ASSET_PROXY_ALREADY_EXISTS" | ||
); | ||
if (currentAssetProxy != 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.
Let's use brackets after all if statements please.
); | ||
|
||
if (assetProxy == address(0)) | ||
rrevert(AssetProxyDispatchError(AssetProxyDispatchErrorCodes.UNKNOWN_ASSET_PROXY)); |
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 wonder if both of these errors AssetProxyDispatcher
errors should just rethrow the entire assetData
. Thoughts @fabioberger ?
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'd be in favor. Imagine you have an order that fails with the error. If we include the assetData
that triggered these failures, it'll be easier to debug.
rrevert( | ||
SignatureError( | ||
orderInfo.orderHash, | ||
SignatureErrorCodes.BAD_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.
Hmm, also wondering if the signature errors should be more specific. In general, it feels like we need to make some assumptions about what information the end user actually has.
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 actually think this is fine. If logic inside the isValidSignature
reverts, they will get a detailed error message for those specific issues. This one comes to down to ecrecover
returning a different address, it not being presigned or the wallet contract returning false
. Which one of those occured can be deduced by looking at the signature type passed in.
); | ||
|
||
if (takerAssetFillAmount == 0) | ||
rrevert(FillError(orderInfo.orderHash, FillErrorCodes.INVALID_TAKER_AMOUNT)); |
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 use rrevert
over rrequire
?
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 would look like rrequire(takerAssetFillAmount != 0, FillError(...))
. It's a nicer experience, but it means the revert reason has to be ABI encoded before the condition is checked, prematurely wasting a lot of 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.
That answers my prev question. Makes sense
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.
Solidity could potentially fix that on the language level later.
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 about smth like condition || rrevert(Error(stuff))
? Will it be lazy-evaluated?
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. Will check on remix.
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.
Yup, condition || rrevert(Error(stuff));
works. So that's an option if people want a more compact alternative.
|
||
|
||
// This should probably be moved to contracts-exchange-lib | ||
contract MRichErrors |
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.
Some thoughts on naming here:
- Contracts with the
M
prefix in themixins
directory don't contain actual implementations, just the function signatures (this is so that we can easily mock out the functions in tests, which we don't actually really do yet). The actual implementation of the contract would be prefixed withMixin
. - Since we'll have these in different packages going forward, maybe the name should be more specific to avoid naming collisions (like
MixinExchangeRichErrors
).
pure | ||
{ | ||
assembly { | ||
if iszero(and(success, 0x00000000000000000000000000000000000000000000000000000000000000FF)) { |
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 think we actually need this mask?
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 my concern: https://solidity.readthedocs.io/en/v0.5.5/security-considerations.html#minor-details
I tried it out in remix and function arguments don't get bitmasked before hitting the body, so the dirty higher bits passed into rrequire()
persist.
/// may be preferable (gas-wise) to rrequire() | ||
/// since you can avoid the needless abi encoding if the test passes. | ||
/// @param errorData ABI encoded error data. | ||
function rrevert(bytes memory errorData) |
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.
rrevert
and rrequire
should probably be in a separate contract in utils
, since these will be reused in other packages.
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.
Definitely. Will move them once all contracts agree on solidity version.
@@ -1,6 +1,6 @@ | |||
{ | |||
"name": "@0x/contracts-exchange", | |||
"version": "1.0.9", | |||
"version": "1.1.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.
You don't actually need to bump versions in package.json (this happens automatically when we publish the packages). It shouldn't matter either way.
/** | ||
* Base type for rich revert reasons. | ||
*/ | ||
export abstract class RichRevertReason { |
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.
Does it make sense for this to be in the order-utils
package? contract-wrappers
feels more natural to me since we're decoding our contact's reverts specifically.
I wonder if it would make sense to use a generic signature, like Example:
If this could be beneficial, it's worth noting that it shouldn't be too much additional cost. We just prepend the selector, a generic offset ( Edit: |
@hysz I like this. Much less chance of collisions with the selector bytes. |
"note": "Add rich revert reasons" | ||
} | ||
] | ||
}, |
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.
You should append those changes to the previous entry. It doesn't have the timestamp which means - it's not published. Just change the 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.
Also add the PR number please 🙏
"ASSET_PROXY_ALREADY_EXISTS" | ||
); | ||
if (currentAssetProxy != address(0)) | ||
rrevert(AssetProxyExistsError(currentAssetProxy)); |
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 like the require pattern. I think we should have smth like rrequire
); | ||
|
||
if (takerAssetFillAmount == 0) | ||
rrevert(FillError(orderInfo.orderHash, FillErrorCodes.INVALID_TAKER_AMOUNT)); |
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.
That answers my prev question. Makes sense
); | ||
|
||
if (takerAssetFillAmount == 0) | ||
rrevert(FillError(orderInfo.orderHash, FillErrorCodes.INVALID_TAKER_AMOUNT)); |
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.
Solidity could potentially fix that on the language level later.
); | ||
|
||
if (takerAssetFillAmount == 0) | ||
rrevert(FillError(orderInfo.orderHash, FillErrorCodes.INVALID_TAKER_AMOUNT)); |
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 about smth like condition || rrevert(Error(stuff))
? Will it be lazy-evaluated?
RICH_REVERT_REGISTRY, | ||
); | ||
|
||
const DECODER_CACHE: ObjectMap<Decoder> = _.zipObject( |
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.
Our naming convention for Objects is stuffToStuff1ToStuff2 etc...
For example selectorToDecoder
_.map(RICH_REVERT_REGISTRY, r => createDecoder(r.abi)), | ||
); | ||
|
||
function checkArgEquality(type: string, a: ArgTypes, b: ArgTypes): boolean { |
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: Maybe name those params lhs
and rhs
?left hand side
}; | ||
} | ||
|
||
function declarationToAbi(decl: string): RichRevertAbi { |
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.
Ethers.js should have a function to parse the ABI string. Check my prev PR
const assert = chai.assert; | ||
|
||
// tslint:disable:custom-no-magic-numbers | ||
describe.only('RichRevertReasons', () => { |
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.
describe.only('RichRevertReasons', () => { | |
describe('RichRevertReasons', () => { |
"note": "Move OrderStatus enum from /contracts/test-utils into 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.
Merge with prev entry
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.
And PR # :)
"note": "Add rich revert reasons" | ||
} | ||
] | ||
}, |
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.
Also add the PR number please 🙏
); | ||
|
||
if (assetProxy == address(0)) | ||
rrevert(AssetProxyDispatchError(AssetProxyDispatchErrorCodes.UNKNOWN_ASSET_PROXY)); |
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'd be in favor. Imagine you have an order that fails with the error. If we include the assetData
that triggered these failures, it'll be easier to debug.
rrevert( | ||
SignatureError( | ||
orderInfo.orderHash, | ||
SignatureErrorCodes.BAD_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.
I actually think this is fine. If logic inside the isValidSignature
reverts, they will get a detailed error message for those specific issues. This one comes to down to ecrecover
returning a different address, it not being presigned or the wallet contract returning false
. Which one of those occured can be deduced by looking at the signature type passed in.
); | ||
|
||
if (takerAssetFilledAmount > takerAssetFillAmount) | ||
rrevert(FillError(orderInfo.orderHash, FillErrorCodes.TAKER_OVERPAY)); |
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 def. be using curly brackets for all conditionals.
if (safeMul(makerAssetFilledAmount, order.takerAssetAmount) | ||
> | ||
safeMul(order.makerAssetAmount, takerAssetFilledAmount)) { | ||
rrevert(FillError(orderInfo.orderHash, FillErrorCodes.INVALID_FILL_PRICE)); |
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.
Technically we'd need takerAssetFilledAmount
and makerAssetFilledAmount
here since without those values, one couldn't figure out what went wrong. However, this is a cautionary assertion that should never hit, so maybe we can pass on this one.
@@ -1,6 +1,6 @@ | |||
{ | |||
"name": "@0x/order-utils", | |||
"version": "7.0.2", | |||
"version": "7.1.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.
Revert. You get the drill ;)
@@ -72,3 +72,5 @@ export { | |||
FeeOrdersAndRemainingFeeAmount, | |||
OrdersAndRemainingFillAmount, | |||
} from './types'; | |||
|
|||
export * from './rich_reverts'; |
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'd rather we explicitly list the items we want exported. Someone might add an export to that file and be surprise to find that it's been auto-magically exported by the package.
new IncompleteFillError(), | ||
]; | ||
|
||
const RICH_REVERT_LUT: ObjectMap<RichRevertReason> = _.zipObject( |
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.
Please use readable variable names over conciseness.
|
||
describe('decoding', () => { | ||
interface DecoderTestData { | ||
cls: { name: string }; |
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.
cls
?
"note": "Move OrderStatus enum from /contracts/test-utils into 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.
And PR # :)
Description
This is a proof-of-concept migration from regular string-based revert reasons to much more informative, ABI-encoded revert reasons. This is based around discussions of EIP 838.
NOTE I've only migrated the 0.4.x version of the
exchange
contracts because ATM other contract packages indevelopment
use a conflicting 0.5.x version of Solidity. Once PR #1682 for modernizing all the legacy contracts gets merged, I can start migrating en masse. But before then, we should have a discussion about this approach, because the implementation has some pain points and touches several packages.Some Background
Current Solidity revert strings are already returned as calldata ABI-encoded bytes with a method signature of
Error(string)
, but you can actually return any arbitrary bytes with an assembly call torevert()
.Revert data can only be reliably recovered during an
eth_call
, because transactions only produce a hash and receipt, with a single status bit indicating success or failure. But a significant issue we've run into is just detecting arevert
during aneth_call
, particularly with Geth. A Geth node will simply return the revert bytes, with no signal that the call reverted. Parity supposedly returns a signal, but, as we don't currently test against Parity, I cannot personally confirm this. The current hack (already implemented in/packages/base-contract
is to basically check if the returned data starts with thebytes4
selector of the standard Error signature.Where We Are Now
There have been discussions around various schemes of returning rich revert data. The EVM does not seem to sanitize what gets returned via
revert()
, so many options are on the table. Ultimately, for this PoC, we've settled on simply ABI-encoding against custom method signatures for specific Exchange errors. One common example would beOrderStatusError(string,uint8)
, which encodes an order's hash and current status. This is thrown whenever the order is in an incompatible state during an operation.While this approach is relatively simple and wire-compact, it does require prior knowledge of all revert signatures to semi-confidently discern which return values are true reverts and which are just arbitrary bytes. It is also not backwards compatible-- clients that only look for
Error(string)
signatures will likely either fail at the decoding step or miss these reverts entirely.Significant additions have been made to the
order-utils
,test-utils
, andabi-gen-templates
packages to help intercept, decode, construct, and test against these new revert formats. Small modifications were also made to other sundry packages so they could anticipate the new reverts and pass their tests.For the Solidity implementation, please take a look at /contracts/exchange/.../MRichErrors.sol. Usage is peppered among the various Exchange contracts (everywhere there used to be a
require()
orrevert()
).The Typescript implementation currently resides at `/packages/order-utils/.../rich_reverts.ts. Some tests also exist to demonstrate usage (though more could be used).
There is also a chai extension implemented in
contract-wrappers
that demonstrates integration with chai. This will probably be moved totest-utils
in future commits because that makes more sense.Testing instructions
Just run the regular monorepo test suite since this PR spans many packages.
Types of changes
Checklist:
[WIP]
if necessary.