Summary
On December 5, 2023, Web3 development platform Thirdweb reported security vulnerabilities in their pre-built smart contracts. This flaw impacted all ERC20, ERC721, and ERC1155 tokens deployed using these contracts. In the following days, tokens deployed with the vulnerable contracts were progressively exploited in attacks.
Vulnerability Analysis
ERC-2771
This EIP defines a contract-level protocol for Recipient
contracts to accept meta-transactions
through trusted Forwarder
contracts. No protocol changes are made. Recipient contracts are sent the effective msg.sender
(referred to as _msgSender()
) and msg.data
(referred to as _msgData()
) by appending additional calldata.
In practice, OpenZeppelin's implementation ERC2771Context
is widely used. Specifically, the last 20 bytes of calldata from the trusted forwarder are treated as _msgSender()
. For common library users, it seems they only need to replace all uses of msg.sender
with _msgSender()
.
function _msgSender() internal view virtual override returns (address) {
uint256 calldataLength = msg.data.length;
uint256 contextSuffixLength = _contextSuffixLength();
if (isTrustedForwarder(msg.sender) && calldataLength >= contextSuffixLength) {
return address(bytes20(msg.data[calldataLength - contextSuffixLength:]));
} else {
return super._msgSender();
}
}
function _msgData() internal view virtual override returns (bytes calldata) {
uint256 calldataLength = msg.data.length;
uint256 contextSuffixLength = _contextSuffixLength();
if (isTrustedForwarder(msg.sender) && calldataLength >= contextSuffixLength) {
return msg.data[:calldataLength - contextSuffixLength];
} else {
return super._msgData();
}
}
Multicall
Users can utilize multicall to integrate multiple calls into one single call. The calldatas of multiple calls are extracted from the calldata of a single call. In practice, OpenZeppelin's MulticallUpgradeable
is widely used. (The bug is fixed now)
function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results) {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
results[i] = _functionDelegateCall(address(this), data[i]);
}
return results;
}
ERC-2771 + Multicall
The issue arises due to inconsistencies in how calldata is packed and unpacked between these two components (ERC-2771 and Multicall). As per ERC-2771, the trusted forwarder should pack the message data and sender information together. The contract then uses _msgData()
and _msgSender()
to unpack the message data and sender information, respectively.
However, the multicall function is not compatible with how ERC-2771 packs data. If implemented correctly, Multicall
should use _msgSender()
to unpack the sender information and append it to each message's call data so that it can be unpacked properly in subsequent calls. But this step in the actual implemeantion is missed.
Meanwhile, the contract following ERC-2771 will try to unpack message data and sender information using _msgData()
and _msgSender()
. However, without the sender information appended to calldata, the sender information is unpacked as the last 20 bytes of _msgData()
, which the attacker can control. This allows an attacker to construct manipulated calldata that executes malicious logic with an arbitrary sender information, violating the expectations set by the specifications.
Attack Analysis
Let's take one instance of an attack transaction as an example.
- Step 1: Swap 5 $WETH for 3,455,399,346 $TIME.
- Step 2: Invoke trusted forwarder with carefully crafted data, after being parsed by multicall, the burn function is called with Uniswap Pool as the
_msgSender()
. The $TIME balance of pool is decreased. - Step 3: Sync the pool, the price of $TIME is lifted.
- Step 4: Swap 3,455,399,346 $TIME for 94 $WETH.
The step 2 is the key of attack. Attacker invoke Forwarder.execute to forward data:
However, it is parsed as bytes[] of length 1, then used as data to call the contract.
Highlight & Lesson
In the DeFi space, it is crucial to pay attention to the security of third-party libraries. Unfortunately, these libraries can sometimes interact with each other in unexpected and covert ways, posing a threat to the security of the contract. This highlights the importance of thorough auditing and monitoring to mitigate any potential vulnerabilities that may arise from such interactions.
Read other articles in this series:
- Lead-In: Top Ten "Awesome" Security Incidents in 2023
- #1: Harvesting MEV Bots by Exploiting Vulnerabilities in Flashbots Relay
- #2: Euler Finance Incident: The Largest Hack of 2023
- #3: KyberSwap Incident: Masterful Exploitation of Rounding Errors with Exceedingly Subtle Calculations
- #4: Curve Incident: Compiler Error Produces Faulty Bytecode from Innocent Source Code
- #5: Platypus Finance: Surviving Three Attacks with a Stroke of Luck
- #6: Hundred Finance Incident: Catalyzing the Wave of Precision-Related Exploits in Vulnerable Forked Protocols
- #7: ParaSpace Incident: A Race Against Time to Thwart the Industry's Most Critical Attack Yet
- #8: SushiSwap Incident: A Clumsy Rescue Attempt Leads to a Series of Copycat Attacks
- #9: MEV Bot 0xd61492: From Predator to Prey in an Ingenious Exploit