⚠️ERC-2771 Delegatecall Vulnerability
Make sure your contracts are not affected by the recently disclosed vulnerability
Please read the following resources from OpenZeppelin and ThirdWeb explaining the vulnerability:
Vulnerability explained
ERC-2771 is a standard enabling contracts to authenticate users during transaction relaying. Before delving into the security risks of its implementation, it is crucial to understand the mechanics of the ERC-2771 flow.
ERC-2771 Overview
User Request Signing
The user signs their request and incorporates this signature into the payload.
Relay Contract Verification
The relay contract validates the signature and appends the user's 20-byte address to the end of the
calldata
.
Target Contract Decoding
The target contract decodes the user address by extracting the last 20 bytes from the
calldata
, but only whenmsg.sender
is the relay contract, known as the Trusted Forwarder.Decoding is done using assembly for efficiency, as shown in the following code snippet:
Risks of delegatecall
delegatecall
Context Preservation in delegatecall
When Contract A invokes Contract B using
delegatecall()
,msg.sender
in Contract B remains the original caller, asdelegatecall()
preserves the caller's context.
Address Extraction in ERC-2771
As outlined above, extracting the original user address involves verifying that
msg.sender
is the Trusted Forwarder, then retrieving the user address from the final 20 bytes ofcallData
.
ERC-2771 Relayer Specifics
If an ERC-2771 Relayer is employed and the target method uses
delegatecall()
to its own address (address(this).delegatecall(...)
), the Trusted Forwarder check will always pass, asmsg.sender
will consistently be the Gelato Relay Contract.In scenarios where the target method modifies the
calldata
, it becomes uncertain whether the last 20 bytes accurately represent the original user when_msgSender()
is invoked.
🚨 If you're implementing
delegatecall()
in conjunction with ERC-2771, please reach out to us for assistance. We'll help ensure that your implementation is robust and secure.
Vulnerability conditions
The vulnerability described arises when all three of the following conditions are met in a smart contract. It's crucial to avoid these conditions concurrently.
Avoid the following conditions in the same smart contract:
Implementation of ERC2771Context or assumptions on data from the trusted forwarder: the contract either implements ERC2771Context or operates under the assumption that data from the trusted forwarder will be appended to and subsequently extracted from the
calldata
.Use of delegatecall to Self-Contract: the contract uses
delegatecall
to call itself, typically indicated byaddress(this).delegatecall(...)
.Calldata manipulation: situations involving the manipulation of
calldata
, common in functions likemulticall
.
🚨 Avoid multicall
in combination with ERC-2771
multicall
in combination with ERC-2771The vulnerability is evident in a typical multicall
function, structured as follows:
Vulnerability Mechanism
Within the loop,
delegateCall()
is executed, targeting the contract itself (address(this).delegatecall(data[i]
).When
_msgSender()
is evaluated within this call, it does not return the original user who signed the transaction. Instead, it yields the last 20 bytes ofdata[i]
.
Potential for Exploitation
A malicious actor could exploit this by appending a victim's address at the end of
data[i]
.As a result,
_msgSender()
would erroneously identify the victim's address as the validated user who signed the transaction, leading to potential security breaches.
✅ Safe multicall
& ERC-2771 implementation
multicall
& ERC-2771 implementationTo securely implement multicall
in conjunction with ERC-2771, it is recommended to manually append the context to each data[i]
, as outlined in OpenZeppelin's blog. The approach involves the following steps:
Key Points
Context Determination: The context is derived by comparing
msg.sender
and_msgSender()
. If they match, no additional context is appended. Otherwise, the last 20 bytes ofmsg.data
are used.Secure Delegatecall: By appending the context to each
data[i]
before thedelegatecall
, the function ensures that the original sender's address is correctly interpreted in subsequent calls.Robust Error Handling: The use of
require(success)
after eachdelegatecall
ensures that any call that fails will halt the execution, maintaining the integrity of the operation.
Last updated