Skip to content
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

Add abi.encodeError #14974

Closed
wants to merge 15 commits into from
1 change: 1 addition & 0 deletions Changelog.md
Expand Up @@ -2,6 +2,7 @@

Language Features:
* Introduce a new overload ``require(bool, Error)`` that allows usage of ``require`` functions with custom errors. This feature is available in the ``via-ir`` pipeline only.
* General: New builtin function ``abi.encodeError(customError, (arg1, arg2, ...))`` that type-checks the arguments and returns the ABI-encoded error return data.


Compiler Features:
Expand Down
7 changes: 4 additions & 3 deletions docs/cheatsheet.rst
Expand Up @@ -11,8 +11,8 @@ Order of Precedence of Operators

.. index:: abi;decode, abi;encode, abi;encodePacked, abi;encodeWithSelector, abi;encodeCall, abi;encodeWithSignature

ABI Encoding and Decoding Functions
===================================
ABI Encoding and Decoding Functions and Errors
==============================================

- ``abi.decode(bytes memory encodedData, (...)) returns (...)``: :ref:`ABI <ABI>`-decodes
the provided data. The types are given in parentheses as second argument.
Expand All @@ -26,6 +26,8 @@ ABI Encoding and Decoding Functions
tuple. Performs a full type-check, ensuring the types match the function signature. Result equals ``abi.encodeWithSelector(functionPointer.selector, (...))``
- ``abi.encodeWithSignature(string memory signature, ...) returns (bytes memory)``: Equivalent
to ``abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)``
- ``abi.encodeError(error errorPointer, (...)) returns (bytes memory)``: ABI-encodes the revert data for ``errorPointer`` with the arguments found in the
tuple. Performs a full type-check, ensuring the types match the error definition. Results equals ``abi.encodeWithSelector(errorPointer.selector, ...)``

.. index:: bytes;concat, string;concat

Expand Down Expand Up @@ -167,4 +169,3 @@ Modifiers
behavior to be changed in derived contracts.
- ``override``: States that this function, modifier or public state variable changes
the behavior of a function or modifier in a base contract.

7 changes: 4 additions & 3 deletions docs/units-and-global-variables.rst
Expand Up @@ -135,15 +135,16 @@ Block and Transaction Properties

.. index:: abi, encoding, packed

ABI Encoding and Decoding Functions
-----------------------------------
ABI Encoding and Decoding Functions and Errors
----------------------------------------------

- ``abi.decode(bytes memory encodedData, (...)) returns (...)``: ABI-decodes the given data, while the types are given in parentheses as second argument. Example: ``(uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))``
- ``abi.encode(...) returns (bytes memory)``: ABI-encodes the given arguments
- ``abi.encodePacked(...) returns (bytes memory)``: Performs :ref:`packed encoding <abi_packed_mode>` of the given arguments. Note that packed encoding can be ambiguous!
- ``abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory)``: ABI-encodes the given arguments starting from the second and prepends the given four-byte selector
- ``abi.encodeWithSignature(string memory signature, ...) returns (bytes memory)``: Equivalent to ``abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)``
- ``abi.encodeCall(function functionPointer, (...)) returns (bytes memory)``: ABI-encodes a call to ``functionPointer`` with the arguments found in the tuple. Performs a full type-check, ensuring the types match the function signature. Result equals ``abi.encodeWithSelector(functionPointer.selector, (...))``
- ``abi.encodeCall(function functionPointer, (...)) returns (bytes memory)``: ABI-encodes a call to ``functionPointer`` with the arguments found in the tuple. Performs a full type-check, ensuring the types match the function signature. Result equals ``abi.encodeWithSelector(functionPointer.selector, ...)``
- ``abi.encodeError(function errorPointer, (...)) returns (bytes memory)``: ABI-encodes the revert data for ``errorPointer`` with the arguments found in the tuple. Performs a full type-check, ensuring the types match the error definition. Result equals ``abi.encodeWithSelector(errorPointer.selector, ...)``

.. note::
These encoding functions can be used to craft data for external function calls without actually
Expand Down
102 changes: 82 additions & 20 deletions libsolidity/analysis/TypeChecker.cpp
Expand Up @@ -2203,6 +2203,7 @@ void TypeChecker::typeCheckABIEncodeFunctions(
_functionType->kind() == FunctionType::Kind::ABIEncodePacked ||
_functionType->kind() == FunctionType::Kind::ABIEncodeWithSelector ||
_functionType->kind() == FunctionType::Kind::ABIEncodeCall ||
_functionType->kind() == FunctionType::Kind::ABIEncodeError ||
_functionType->kind() == FunctionType::Kind::ABIEncodeWithSignature,
"ABI function has unexpected FunctionType::Kind."
);
Expand All @@ -2227,10 +2228,13 @@ void TypeChecker::typeCheckABIEncodeFunctions(
// Perform standard function call type checking
typeCheckFunctionGeneralChecks(_functionCall, _functionType);

// No further generic checks needed as we do a precise check for ABIEncodeCall
if (_functionType->kind() == FunctionType::Kind::ABIEncodeCall)
// No further generic checks needed as we do a precise check for ABIEncodeCall and ABIEncodeError
if (
_functionType->kind() == FunctionType::Kind::ABIEncodeCall ||
_functionType->kind() == FunctionType::Kind::ABIEncodeError
)
{
typeCheckABIEncodeCallFunction(_functionCall);
typeCheckABIEncodeCallFunctionOrError(_functionCall, _functionType);
return;
}

Expand Down Expand Up @@ -2292,19 +2296,34 @@ void TypeChecker::typeCheckABIEncodeFunctions(
}
}

void TypeChecker::typeCheckABIEncodeCallFunction(FunctionCall const& _functionCall)
void TypeChecker::typeCheckABIEncodeCallFunctionOrError(
FunctionCall const& _functionCall,
FunctionTypePointer _functionType
)
{
std::vector<ASTPointer<Expression const>> const& arguments = _functionCall.arguments();

// Expecting first argument to be the function pointer and second to be a tuple.
if (arguments.size() != 2)
{
m_errorReporter.typeError(
6219_error,
_functionCall.location(),
"Expected two arguments: a function pointer followed by a tuple."
);
return;
switch (_functionType->kind()) {
case FunctionType::Kind::ABIEncodeCall:
m_errorReporter.typeError(
6219_error,
_functionCall.location(),
"Expected two arguments: a function pointer followed by a tuple."
);
return;
case FunctionType::Kind::ABIEncodeError:
m_errorReporter.typeError(
6220_error,
_functionCall.location(),
"Expected two arguments: a custom error followed by a tuple."
);
return;
default:
solAssert(false);
}
}

FunctionType const* externalFunctionType = nullptr;
Expand All @@ -2316,17 +2335,32 @@ void TypeChecker::typeCheckABIEncodeCallFunction(FunctionCall const& _functionCa
}
else
{
m_errorReporter.typeError(
5511_error,
arguments.front()->location(),
"Expected first argument to be a function pointer, not \"" +
type(*arguments.front())->humanReadableName() +
"\"."
);
return;
switch (_functionType->kind()) {
case FunctionType::Kind::ABIEncodeCall:
m_errorReporter.typeError(
5511_error,
arguments.front()->location(),
"Expected first argument to be a function pointer, not \"" +
type(*arguments.front())->humanReadableName() +
"\"."
);
return;
case FunctionType::Kind::ABIEncodeError:
m_errorReporter.typeError(
5512_error,
arguments.front()->location(),
"Expected first argument to be a custom error, not \"" +
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to think of some test cases and when I try something like:

error E(uint a, uint b);
contract C {
    function g() public view returns (bytes memory) {
        bytes memory b1 = abi.encodeError(E(1, 2), (1, 2));
        return b1;
    }
}

The following error message is generated:
Expected first argument to be a custom error, not "error".
Which is a bit weird. Maybe we should be more precise and change to [...] custom error name [...] ?
I think we should have a test like this anyway.

type(*arguments.front())->humanReadableName() +
"\"."
);
return;
default:
solAssert(false);
}
}

if (
_functionType->kind() == FunctionType::Kind::ABIEncodeCall &&
externalFunctionType->kind() != FunctionType::Kind::External &&
externalFunctionType->kind() != FunctionType::Kind::Declaration
)
Expand Down Expand Up @@ -2378,6 +2412,34 @@ void TypeChecker::typeCheckABIEncodeCallFunction(FunctionCall const& _functionCa
m_errorReporter.typeError(3509_error, arguments[0]->location(), ssl, msg);
return;
}
if (
_functionType->kind() == FunctionType::Kind::ABIEncodeError &&
externalFunctionType->kind() != FunctionType::Kind::Error
)
{
std::string msg = "Expected an error type.";

switch (externalFunctionType->kind())
{
case FunctionType::Kind::Internal:
case FunctionType::Kind::External:
case FunctionType::Kind::Declaration:
case FunctionType::Kind::DelegateCall:
msg += " Cannot use functions for abi.encodeError.";
break;
case FunctionType::Kind::Creation:
msg += " Provided creation function.";
break;
case FunctionType::Kind::Event:
msg += " Cannot use events for abi.encodeError.";
break;
default:
msg += " Cannot use special function.";
}

m_errorReporter.typeError(3510_error, arguments[0]->location(), msg);
return;
}
solAssert(!externalFunctionType->takesArbitraryParameters(), "Function must have fixed parameters.");
// Tuples with only one component become that component
std::vector<ASTPointer<Expression const>> callArguments;
Expand All @@ -2386,7 +2448,7 @@ void TypeChecker::typeCheckABIEncodeCallFunction(FunctionCall const& _functionCa
if (tupleType)
{
if (TupleExpression const* argumentTuple = dynamic_cast<TupleExpression const*>(arguments[1].get()))
callArguments = decltype(callArguments){argumentTuple->components().begin(), argumentTuple->components().end()};
callArguments = {argumentTuple->components().begin(), argumentTuple->components().end()};
else
{
m_errorReporter.typeError(
Expand Down Expand Up @@ -2445,7 +2507,6 @@ void TypeChecker::typeCheckABIEncodeCallFunction(FunctionCall const& _functionCa
}
}


void TypeChecker::typeCheckStringConcatFunction(
FunctionCall const& _functionCall,
FunctionType const* _functionType
Expand Down Expand Up @@ -2903,6 +2964,7 @@ bool TypeChecker::visit(FunctionCall const& _functionCall)
case FunctionType::Kind::ABIEncodeWithSelector:
case FunctionType::Kind::ABIEncodeWithSignature:
case FunctionType::Kind::ABIEncodeCall:
case FunctionType::Kind::ABIEncodeError:
{
typeCheckABIEncodeFunctions(_functionCall, functionType);
returnTypes = functionType->returnParameterTypes();
Expand Down
7 changes: 5 additions & 2 deletions libsolidity/analysis/TypeChecker.h
Expand Up @@ -110,8 +110,11 @@ class TypeChecker: private ASTConstVisitor
FunctionTypePointer _functionType
);

/// Performs checks specific to the ABI encode functions of type ABIEncodeCall
void typeCheckABIEncodeCallFunction(FunctionCall const& _functionCall);
/// Performs checks specific to the ABI encode functions/errors of type ABIEncodeCall/AbiEncodeError
void typeCheckABIEncodeCallFunctionOrError(
FunctionCall const& _functionCall,
FunctionTypePointer _functionType
);

/// Performs general checks and checks specific to string concat function call
void typeCheckStringConcatFunction(
Expand Down
1 change: 1 addition & 0 deletions libsolidity/analysis/ViewPureChecker.cpp
Expand Up @@ -390,6 +390,7 @@ void ViewPureChecker::endVisit(MemberAccess const& _memberAccess)
{MagicType::Kind::ABI, "encodePacked"},
{MagicType::Kind::ABI, "encodeWithSelector"},
{MagicType::Kind::ABI, "encodeCall"},
{MagicType::Kind::ABI, "encodeError"},
{MagicType::Kind::ABI, "encodeWithSignature"},
{MagicType::Kind::Message, "data"},
{MagicType::Kind::Message, "sig"},
Expand Down
12 changes: 12 additions & 0 deletions libsolidity/ast/Types.cpp
Expand Up @@ -3075,6 +3075,7 @@ std::string FunctionType::richIdentifier() const
case Kind::ABIEncodePacked: id += "abiencodepacked"; break;
case Kind::ABIEncodeWithSelector: id += "abiencodewithselector"; break;
case Kind::ABIEncodeCall: id += "abiencodecall"; break;
case Kind::ABIEncodeError: id += "abiencodeerror"; break;
case Kind::ABIEncodeWithSignature: id += "abiencodewithsignature"; break;
case Kind::ABIDecode: id += "abidecode"; break;
case Kind::BlobHash: id += "blobhash"; break;
Expand Down Expand Up @@ -3663,6 +3664,7 @@ bool FunctionType::isPure() const
m_kind == Kind::ABIEncodePacked ||
m_kind == Kind::ABIEncodeWithSelector ||
m_kind == Kind::ABIEncodeCall ||
m_kind == Kind::ABIEncodeError ||
m_kind == Kind::ABIEncodeWithSignature ||
m_kind == Kind::ABIDecode ||
m_kind == Kind::MetaType ||
Expand Down Expand Up @@ -4179,6 +4181,16 @@ MemberList::MemberMap MagicType::nativeMembers(ASTNode const*) const
nullptr,
FunctionType::Options::withArbitraryParameters()
)},
{"encodeError", TypeProvider::function(
TypePointers{},
TypePointers{TypeProvider::array(DataLocation::Memory)},
strings{},
strings{1, ""},
FunctionType::Kind::ABIEncodeError,
StateMutability::Pure,
nullptr,
FunctionType::Options::withArbitraryParameters()
)},
{"encodeWithSignature", TypeProvider::function(
TypePointers{TypeProvider::array(DataLocation::Memory, true)},
TypePointers{TypeProvider::array(DataLocation::Memory)},
Expand Down
1 change: 1 addition & 0 deletions libsolidity/ast/Types.h
Expand Up @@ -1284,6 +1284,7 @@ class FunctionType: public Type
ABIEncodePacked,
ABIEncodeWithSelector,
ABIEncodeCall,
ABIEncodeError,
ABIEncodeWithSignature,
ABIDecode,
GasLeft, ///< gasleft()
Expand Down
17 changes: 14 additions & 3 deletions libsolidity/codegen/ExpressionCompiler.cpp
Expand Up @@ -1304,20 +1304,25 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall)
case FunctionType::Kind::ABIEncodePacked:
case FunctionType::Kind::ABIEncodeWithSelector:
case FunctionType::Kind::ABIEncodeCall:
case FunctionType::Kind::ABIEncodeError:
case FunctionType::Kind::ABIEncodeWithSignature:
{
bool const isPacked = function.kind() == FunctionType::Kind::ABIEncodePacked;
bool const hasSelectorOrSignature =
function.kind() == FunctionType::Kind::ABIEncodeWithSelector ||
function.kind() == FunctionType::Kind::ABIEncodeCall ||
function.kind() == FunctionType::Kind::ABIEncodeError ||
function.kind() == FunctionType::Kind::ABIEncodeWithSignature;

TypePointers argumentTypes;
TypePointers targetTypes;

ASTNode::listAccept(arguments, *this);

if (function.kind() == FunctionType::Kind::ABIEncodeCall)
if (
function.kind() == FunctionType::Kind::ABIEncodeCall ||
function.kind() == FunctionType::Kind::ABIEncodeError
)
{
solAssert(arguments.size() == 2);

Expand Down Expand Up @@ -1398,10 +1403,16 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall)
dataOnStack = TypeProvider::fixedBytes(32);
}
}
else if (function.kind() == FunctionType::Kind::ABIEncodeCall)
else if (
function.kind() == FunctionType::Kind::ABIEncodeCall ||
function.kind() == FunctionType::Kind::ABIEncodeError
)
{
auto const& funType = dynamic_cast<FunctionType const&>(*selectorType);
if (funType.kind() == FunctionType::Kind::Declaration)
if (
funType.kind() == FunctionType::Kind::Declaration ||
funType.kind() == FunctionType::Kind::Error
)
{
solAssert(funType.hasDeclaration());
solAssert(selectorType->sizeOnStack() == 0);
Expand Down
24 changes: 19 additions & 5 deletions libsolidity/codegen/ir/IRGeneratorForStatements.cpp
Expand Up @@ -1184,13 +1184,15 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall)
case FunctionType::Kind::ABIEncodePacked:
case FunctionType::Kind::ABIEncodeWithSelector:
case FunctionType::Kind::ABIEncodeCall:
case FunctionType::Kind::ABIEncodeError:
case FunctionType::Kind::ABIEncodeWithSignature:
{
bool const isPacked = functionType->kind() == FunctionType::Kind::ABIEncodePacked;
solAssert(functionType->padArguments() != isPacked);
bool const hasSelectorOrSignature =
functionType->kind() == FunctionType::Kind::ABIEncodeWithSelector ||
functionType->kind() == FunctionType::Kind::ABIEncodeCall ||
functionType->kind() == FunctionType::Kind::ABIEncodeError ||
functionType->kind() == FunctionType::Kind::ABIEncodeWithSignature;

TypePointers argumentTypes;
Expand All @@ -1199,7 +1201,10 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall)
std::string selector;
std::vector<ASTPointer<Expression const>> argumentsOfEncodeFunction;

if (functionType->kind() == FunctionType::Kind::ABIEncodeCall)
if (
functionType->kind() == FunctionType::Kind::ABIEncodeCall ||
functionType->kind() == FunctionType::Kind::ABIEncodeError
)
{
solAssert(arguments.size() == 2);
// Account for tuples with one component which become that component
Expand Down Expand Up @@ -1227,7 +1232,10 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall)
argumentVars += IRVariable(*argument).stackSlots();
}

if (functionType->kind() == FunctionType::Kind::ABIEncodeCall)
if (
functionType->kind() == FunctionType::Kind::ABIEncodeCall ||
functionType->kind() == FunctionType::Kind::ABIEncodeError
)
{
auto encodedFunctionType = dynamic_cast<FunctionType const*>(arguments.front()->annotation().type);
solAssert(encodedFunctionType);
Expand All @@ -1240,10 +1248,16 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall)
targetTypes.emplace_back(type(*argument).fullEncodingType(false, true, isPacked));


if (functionType->kind() == FunctionType::Kind::ABIEncodeCall)
if (
functionType->kind() == FunctionType::Kind::ABIEncodeCall ||
functionType->kind() == FunctionType::Kind::ABIEncodeError
)
{
auto const& selectorType = dynamic_cast<FunctionType const&>(type(*arguments.front()));
if (selectorType.kind() == FunctionType::Kind::Declaration)
if (
selectorType.kind() == FunctionType::Kind::Declaration ||
selectorType.kind() == FunctionType::Kind::Error
)
{
solAssert(selectorType.hasDeclaration());
selector = formatNumber(selectorType.externalIdentifier() << (256 - 32));
Expand Down Expand Up @@ -1986,7 +2000,7 @@ void IRGeneratorForStatements::endVisit(MemberAccess const& _memberAccess)

define(_memberAccess) << requestedValue << "\n";
}
else if (std::set<std::string>{"encode", "encodePacked", "encodeWithSelector", "encodeCall", "encodeWithSignature", "decode"}.count(member))
else if (std::set<std::string>{"encode", "encodePacked", "encodeWithSelector", "encodeCall", "encodeError", "encodeWithSignature", "decode"}.count(member))
{
// no-op
}
Expand Down