I want to get the revert reason of a failed call
and then use that revert reason in the same on-chain transaction. Is this possible?
5 Answers
I modified shanes answer:
function _getRevertMsg(bytes memory _returnData) internal pure returns (string memory) {
// If the _res length is less than 68, then the transaction failed silently (without a revert message)
if (_returnData.length < 68) return 'Transaction reverted silently';
assembly {
// Slice the sighash.
_returnData := add(_returnData, 0x04)
}
return abi.decode(_returnData, (string)); // All that remains is the revert string
}
It seems to work and that way we dont need the extra library
-
22Uniswap used your code. Wanted you to know. github.com/Uniswap/uniswap-v3-periphery/blob/v1.0.0/contracts/… Commented May 17, 2021 at 9:47
Yes, this is possible. In short, you can use the following code:
(bool success, bytes memory res) = _destination.call.gas(_gasLimit).value(_value)(_data);
// Get the revert message of the call and revert with it if the call failed
if (!success) {
string memory _revertMsg = _getRevertMsg(res);
}
/// @dev Get the revert message from a call
/// @notice This is needed in order to get the human-readable revert message from a call
/// @param _res Response of the call
/// @return Revert message string
function _getRevertMsg(bytes memory _res) internal pure returns (string memory) {
// If the _res length is less than 68, then the transaction failed silently (without a revert message)
if (_res.length < 68) return 'Transaction reverted silently';
bytes memory revertData = _res.slice(4, _res.length - 4); // Remove the selector which is the first 4 bytes
return abi.decode(revertData, (string)); // All that remains is the revert string
}
Breaking that down a little, all unsuccessful calls will return an erroneous response with the signature Error(string)
(0x08c379a). With that in mind, you can simply treat it as any other signature.
if (_res.length < 68) return 'Transaction reverted silently';
The above line will look at the response and return Transaction reverted silently
if there was no revert reason.
bytes memory revertData = _res.slice(4, _res.length - 4);
The above line will derive the revert reason string
from the response in the format bytes
format. As a note, the slice
method is from this BytesLib.sol file.
return abi.decode(revertData, (string));
The above line will decode the data into a string, which is the desired output.
Note: For full disclsure, I am a cofounder of Authereum and those code blocks are from the Authereum contracts that I helped write.
-
2Thanks for the explination. Where does 68 come from? 64 bytes + 4 for the signature? Why at a minimum will a revert lead to more than 64 bytes of data? Commented Aug 20, 2021 at 1:31
-
1
-
2That's because of the way dynamic data is encoded. ABI for solidity reverts is
Error(string)
, so the first 4 bytes will be selector, next 32 bytes will be string offset, next 32 bytes will be string length and all the remaining bytes will be string data. See docs.soliditylang.org/en/latest/… Commented Nov 1, 2021 at 4:59
While the answer from @Paddy looks correct, it screws up the length of _returnData
before passing it to abi.decode
. The screwed up length is much bigger than it should be. Hopefully abi.decode
will just ignore extra bytes after the decoded string, but it would be better not to rely on this, so here is cleaner version:
function extractRevertReason (bytes memory revertData)
internal pure returns (string memory reason) {
uint l = revertData.length;
if (l < 68) return "";
uint t;
assembly {
revertData := add (revertData, 4)
t := mload (revertData) // Save the content of the length slot
mstore (revertData, sub (l, 4)) // Set proper length
}
reason = abi.decode (revertData, (string));
assembly {
mstore (revertData, t) // Restore the content of the length slot
}
}
If only interested in bubbling up the revert then could do:
(success, returnData) = _target.call{value: _value}(_data);
if (!success) {
assembly {
revert(add(returnData, 32), returnData)
}
}
Assembly's revert
takes the memory location of the beginning of the revert data and its length. returnData
points to the beginning of bytes memory
which starts with a length prefix 32 bytes long. After that prefix the actual revert data follows.
There are many cases of reverts:
Panic(uint256)
(Defined since 0.8.0, see https://docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require)Error(string)
(Defined at least since 0.7.0)- Custom errors (Defined since 0.8.0)
- And other cases including revert without reason
A generic way to bubble up the error can be:
// Taken from https://github.com/superfluid-finance/protocol-monorepo/blob/dev/packages/ethereum-contracts/contracts/libs/CallUtils.sol
/// @dev Bubble up the revert from the returnedData (supports Panic, Error & Custom Errors)
/// @notice This is needed in order to provide some human-readable revert message from a call
/// @param returnedData Response of the call
function revertFromReturnedData(bytes memory returnedData) internal pure {
if (returnedData.length < 4) {
// case 1: catch all
revert("CallUtils: target revert()");
} else {
bytes4 errorSelector;
assembly {
errorSelector := mload(add(returnedData, 0x20))
}
if (errorSelector == bytes4(0x4e487b71) /* `seth sig "Panic(uint256)"` */) {
// case 2: Panic(uint256) (Defined since 0.8.0)
// solhint-disable-next-line max-line-length
// ref: https://docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require)
string memory reason = "CallUtils: target panicked: 0x__";
uint errorCode;
assembly {
errorCode := mload(add(returnedData, 0x24))
let reasonWord := mload(add(reason, 0x20))
// [0..9] is converted to ['0'..'9']
// [0xa..0xf] is not correctly converted to ['a'..'f']
// but since panic code doesn't have those cases, we will ignore them for now!
let e1 := add(and(errorCode, 0xf), 0x30)
let e2 := shl(8, add(shr(4, and(errorCode, 0xf0)), 0x30))
reasonWord := or(
and(reasonWord, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000),
or(e2, e1))
mstore(add(reason, 0x20), reasonWord)
}
revert(reason);
} else {
// case 3: Error(string) (Defined at least since 0.7.0)
// case 4: Custom errors (Defined since 0.8.0)
uint len = returnedData.length;
assembly {
revert(add(returnedData, 32), len)
}
}
}
}