-
Notifications
You must be signed in to change notification settings - Fork 504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
OZ L02; Spearbit 87; Enforce strict abi encoding #346
Conversation
Co-authored-by: 0age <[email protected]>
let relativeOffset := sub(params.offset, _bytes.offset) | ||
// Check that that isn't longer than the bytes themselves, or revert | ||
if lt(_bytes.length, add(params.length, relativeOffset)) { | ||
for { let offset := 0 } lt(offset, tailOffset) { offset := add(offset, 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.
exceedingly minor consistency nit: 32
used here whereas elsewhere the hex 0x20
is used
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 love a nit
expectedOffset := add(expectedOffset, length) | ||
} | ||
|
||
// if the data encoding was invalid, or the provided bytes string isnt as long as the encoding says, 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.
isnt
--> isn't
/// @notice equivalent to SliceOutOfBounds.selector, stored in least-significant bits | ||
uint256 constant SLICE_ERROR_SELECTOR = 0x3b99b53d; | ||
|
||
/// @dev equivalent to: abi.decode(params, (bytes, bytes[])) in calldata (requires strict abi encoding) |
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 be better to note here that the masking makes it not quite strictly equivalent.
// Verify actions offset matches strict encoding | ||
let invalidData := xor(calldataload(_bytes.offset), 0x40) | ||
actions.offset := add(_bytes.offset, 0x60) | ||
actions.length := and(calldataload(add(_bytes.offset, 0x40)), OFFSET_OR_LENGTH_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.
Here the action.length
value is silently trimmed to 32 bytes, which could hide errors in abi encoding. Consider doing instead:
actions.length := calldataload(add(_bytes.offset, 0x40))
invalidData := or(invalidData, shr(32, actions.length))
let paramsPtr := add(_bytes.offset, calldataload(add(_bytes.offset, 0x20))) | ||
// Strict encoding requires that the data begin with: | ||
// 0x00: 0x40 (offset to `actions.length`) | ||
// 0x20: 0x60 + actions.length (offset to `params.length`) |
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 comment is inaccurate, as the actions.length
value is actually aligned to the word boundary.
actions.length := calldataload(actionsPtr) | ||
params.length := calldataload(paramsPtr) | ||
// Round actions length up to be word-aligned, and add 0x60 (for the first 3 words of encoding) | ||
let paramsLengthOffset := add(and(add(actions.length, 0x1f), OFFSET_OR_LENGTH_MASK_AND_WORD_ALIGN), 0x60) |
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.
Here an intermediate value is silently trimmed to 32 bits, which could hide overflow errors, and could potentially be exploited. Consider doing instead:
let paramsLengthOffset := shl(5, shr(5, add(actions.length, 0x1f)))
invalidData := or(invalidData, shr(32, paramsLengthOffset))
actions.length := calldataload(actionsPtr) | ||
params.length := calldataload(paramsPtr) | ||
// Round actions length up to be word-aligned, and add 0x60 (for the first 3 words of encoding) | ||
let paramsLengthOffset := add(and(add(actions.length, 0x1f), OFFSET_OR_LENGTH_MASK_AND_WORD_ALIGN), 0x60) |
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 the 0x60
value is added after applying the mask, the result could actually exceed 32 bites.
// Verify params offset matches strict encoding | ||
invalidData := or(invalidData, xor(calldataload(add(_bytes.offset, 0x20)), paramsLengthOffset)) | ||
let paramsLengthPointer := add(_bytes.offset, paramsLengthOffset) | ||
params.length := and(calldataload(paramsLengthPointer), OFFSET_OR_LENGTH_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.
Here the params.length
value is silently trimmed to 32 bytes, which could hide errors in abi encoding. Consider doing instead:
params.length := calldataload(paramsLengthPointer)
invalidData := or(invalidData, shr(32, actions.length))
// The offset of the `_arg`-th element is `32 * arg`, which stores the offset of the length pointer. | ||
// shl(5, x) is equivalent to mul(32, x) | ||
let lengthPtr := | ||
add(_bytes.offset, and(calldataload(add(_bytes.offset, shl(5, _arg))), OFFSET_OR_LENGTH_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.
Overflow is possible when shifting left and when adding. Consider reverting on overflow.
// The offset of the `_arg`-th element is `32 * arg`, which stores the offset of the length pointer. | ||
// shl(5, x) is equivalent to mul(32, x) | ||
let lengthPtr := | ||
add(_bytes.offset, and(calldataload(add(_bytes.offset, shl(5, _arg))), OFFSET_OR_LENGTH_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.
Here the lengthPtr
value is silently trimmed to 32 bytes, which could hide errors in abi encoding. Consider reverting in case the calculated value exceeds 32 bits.
let lengthPtr := | ||
add(_bytes.offset, and(calldataload(add(_bytes.offset, shl(5, _arg))), OFFSET_OR_LENGTH_MASK)) | ||
// the number of bytes in the bytes string | ||
length := and(calldataload(lengthPtr), OFFSET_OR_LENGTH_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.
Here the length
value is silently trimmed to 32 bytes, which could hide errors in abi encoding. Consider reverting in case the length
value exceeds 32 bits.
/// @notice Decode the `_arg`-th element in `_bytes` as `bytes` | ||
/// @param _bytes The input bytes string to extract a bytes string from | ||
/// @param _arg The index of the argument to extract | ||
function toBytes(bytes calldata _bytes, uint256 _arg) internal pure returns (bytes calldata res) { | ||
(uint256 length, uint256 offset) = toLengthOffset(_bytes, _arg); | ||
uint256 length; |
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 variable is redundant. Just use res.length
instead.
// the number of bytes in the bytes string | ||
length := and(calldataload(lengthPtr), OFFSET_OR_LENGTH_MASK) | ||
// the offset where the bytes string begins | ||
let offset := add(lengthPtr, 0x20) |
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 variable is redundant. Just use res.offset
instead.
// 0x60: beginning of actions | ||
|
||
// Verify actions offset matches strict encoding | ||
let invalidData := xor(calldataload(_bytes.offset), 0x40) |
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.
_bytes.offset
is always 0, can save some gas here and below. decodeActionsRouterParams
is always called on calldata that contains only an encoded ABI.
// 0x60: beginning of actions | ||
|
||
// Verify actions offset matches strict encoding | ||
let invalidData := xor(calldataload(_bytes.offset), 0x40) |
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.
Missing test case: failure when the first byte is not 0x40
.
// Round actions length up to be word-aligned, and add 0x60 (for the first 3 words of encoding) | ||
let paramsLengthOffset := add(and(add(actions.length, 0x1f), OFFSET_OR_LENGTH_MASK_AND_WORD_ALIGN), 0x60) | ||
// Verify params offset matches strict encoding | ||
invalidData := or(invalidData, xor(calldataload(add(_bytes.offset, 0x20)), paramsLengthOffset)) |
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 failure here is not tested.
for { let offset := 0 } lt(offset, tailOffset) { offset := add(offset, 32) } { | ||
let itemLengthOffset := calldataload(add(params.offset, offset)) | ||
// Verify that the offset matches the expected offset from strict encoding | ||
invalidData := or(invalidData, xor(itemLengthOffset, expectedOffset)) |
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.
Not tested when fails.
No description provided.