-
Notifications
You must be signed in to change notification settings - Fork 43
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
Make function calls atomic with ERC20 transfers in token supply source subnets #656
Conversation
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 looks correct, but I'm starting to get uncomfortable with the complexity of performCall
. Could we take this opportunity to add unit tests (not integration tests) for performCall
?
assembly { | ||
let returndata_size := mload(ret) | ||
revert(add(32, ret), returndata_size) | ||
} |
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.
Could you comment why the add
opcode? There may be comments in the OZ library, but let's avoid reader indirection.
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.
Ok, I will add comments on the code.
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 the full function from OpenZeppelin. We need to handle the case where there is no return data. The add
is needed to unpack the variable-size return value (first Ethereum word is the size).
function _revert(bytes memory returndata) private pure {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
/// @solidity memory-safe-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert FailedInnerCall();
}
}
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.
From my testing, if the ret
is empty, calling revert(add(32, returndata), returndata_size)
is the same as revert with no data. I think openzeppelin might be emphasising FailedInnerCall
? But for us, if the contract returns empty, we can just return empty. Even if we dont, after some parsing of the error bytes, users will still get an empty ret.
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.
From my testing, if the ret
is empty, calling revert(add(32, returndata), returndata_size)
is the same as revert with no data. I think openzeppelin might be emphasising FailedInnerCall
? But for us, if the contract returns empty, we can just return empty. Even if we dont, after some parsing of the error bytes, users will still get an empty ret.
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.
@cryptoAtwill I suspect you're seeing this because mloading the return data pointer will return the size, which will be 0. You then revert providing an offset equal to the pointer+32, but with size 0. This is probably OK, but I'd rather play it safe and do the check anyway in high level Solidity.
assembly { | ||
let returndata_size := mload(ret) | ||
revert(add(32, ret), returndata_size) | ||
} |
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 the full function from OpenZeppelin. We need to handle the case where there is no return data. The add
is needed to unpack the variable-size return value (first Ethereum word is the size).
function _revert(bytes memory returndata) private pure {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
/// @solidity memory-safe-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert FailedInnerCall();
}
}
|
||
contract SupplySourceHelperTest is Test { | ||
/// Call fails but send value works, both should fail | ||
function test_revert_atomicity() public { |
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 we test performCall
instead, please?
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.
added more tests
} else { | ||
return (success, ret); | ||
revert FailedInnerCall(); | ||
} |
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 remove the else
and simply use revert()
. If the target didn't provide an error, we should not add one into the mix; we're simply interested in passing through here, right?
Make the
performCall
atomic with respect to ERC20 call with value.Close ENG-635