!! Emergency Alert!! Analysis of OpenZeppelin Arbitrary Address Spoofing Attack
Summary
!! Emergency Alert!! Analysis of OpenZeppelin Arbitrary Address Spoofing Attack

Summary
On December 8, 2023, OpenZeppelin issued an important security alert to the community via Twitter, stating that the integration of the ERC-2771 standard with a multicall-like method (wherein the code contains a delegatecall to the contract itself with calldata that can be externally controlled by users) could lead to projects using this pattern being at risk of arbitrary address spoofing attacks.
OpenZeppelin provided methods for identifying vulnerable contracts and mitigating the issue on their blog. For more details, please read the blog post: *https://blog.openzeppelin.com/arbitrary-address-spoofing-vulnerability-erc2771context-multicall-public-disclosure*.
[embed]
Attack Analysis
From OpenZeppelin’s blog, it is evident that several attacks have already occurred. Here we will select one transaction for detailed analysis.
1. Addresses involved in the attack
Attack transaction:
https://etherscan.io/tx/0xecdd111a60debfadc6533de30fb7f55dc5ceed01dfadd30e4a7ebdb416d2f6b6
Attacker address:
0xfde0d1575ed8e06fbf36256bcdfa1f359281455a
Attack contract:
0x6980a47bee930a4584b09ee79ebe46484fbdbdd0
TIME token (Vulnerable contract):
0x4b0e9a7da8bab813efae92a6651019b8bd6c0a29
2. Attack Process
2.1. Attack preparation
-
The attacker approved the Uniswap V2 Router contract to use his TIME tokens.
-
The attacker deposited 5 ether into WETH contract and received 5 WETH.
2.2. Attack execution
-
The attacker called the
swapExactTokensForTokensSupportingFeeOnTransferTokensfunction of the Uniswap V2 Router contract, intending to exchange 5 WETH for TIME token. This swap was conducted in the Uniswap V2: TIME 40 pool and finnally the attacker exchanged for 3455399346.269046 TIME tokens. -
The attacker called the
executefunction of the Forwarder contract, specifying thetoaddress as the TIME contract address anddatain the parameterreq. Another parameter,signature, was a signature made by the address 0xa16a5f37774309710711a8b4e83b068306b21724 (another address of the attacker), andreq.fromwas also this address, so the signature verification of theexecutefunction could pass smoothly. Next, the function of the TIME contract will be called with the calldata consisting ofreq.dataandreq.from. It's important to note that the calldata was carefully constructed by the attacker and included the address of the Uniswap V2: TIME 40 pool. This pool address was where the funds were lost in the subsequent attack.
struct ForwardRequest {
address from;
address to;
uint256 value;
uint256 gas;
uint256 nonce;
bytes data;
}
function execute(ForwardRequest calldata req, bytes calldata signature)
public
payable
returns (bool, bytes memory)
{
require(verify(req, signature), "MinimalForwarder: signature does not match request");
_nonces[req.from] = req.nonce + 1;
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory result) = req.to.call{ gas: req.gas, value: req.value }(
abi.encodePacked(req.data, req.from)
);
if (!success) {
// Next 5 lines from https://ethereum.stackexchange.com/a/83577
if (result.length < 68) revert("Transaction reverted silently");
assembly {
result := add(result, 0x04)
}
revert(abi.decode(result, (string)));
}
// Check gas: https://ronan.eth.link/blog/ethereum-gas-dangers/
assert(gasleft() > req.gas / 63);
return (success, result);
}
- Observing the transaction, we can see that the function being called at this point is the
multicallfunction of the TIME contract. Within themulticallfunction, adelegatecallis made to execute a function within the current contract (TIME). In this transaction, theburnfunction of the TIME contract was called.
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;
}
function _functionDelegateCall(address target, bytes memory data) private returns (bytes memory) {
require(AddressUpgradeable.isContract(target), "Address: delegate call to non-contract");
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returndata) = target.delegatecall(data);
return AddressUpgradeable.verifyCallResult(success, returndata, "Address: low-level delegate call failed");
}
- When the
burnfunction is called, the address for the token to be burned is determined via the_msgSenderfunction. Upon entering the_msgSenderfunction, in this transaction, themsg.senderis the Forwarder contract, which is a forwarder trusted by the TIME contract, and thereforeisTrustedForwarderis true. An address is extracted from the last 20 bytes of the calldata to serve as the sender. The calldata at this point is as follows. The last 20 bytes of the calldata here correspond exactly to the address of the Uniswap V2: TIME 40 pool. Ultimately, 62227259510 TIME tokens from the Uniswap V2: TIME 40 pool were burned.
function burn(uint256 amount) public virtual {
_burn(_msgSender(), amount);
}
function _msgSender() internal view virtual override returns (address sender) {
if (isTrustedForwarder(msg.sender)) {
// The assembly code is more direct than the Solidity version using `abi.decode`.
assembly {
sender := shr(96, calldataload(sub(calldatasize(), 20)))
}
} else {
return super._msgSender();
}
}
0x42966c680000000000000000000000000000000000000000c9112ec16d958e8da8180000760dc1e043d99394a10605b2fa08f123d60faf84
-
The attacker called the
syncfunction of the Uniswap V2: TIME 40 pool, updating the reserves of tokens to match the actual number of tokens in the pool. After the update, the reserve of TIME tokens in the pool decreased, making the price of TIME in the pool more expensive. -
The attacker conducted another exchange in the Uniswap V2: TIME 40 pool, this time swapping TIME tokens for WETH. As the price of TIME tokens had increased, the attacker was able to exchange for a larger amount of WETH. Ultimately, using the 3455399346.269046 TIME acquired from the first exchange, the attacker swapped for approximately 94 WETH, making a profit of over 80 ETH.
Recommendation
It is recommended that all project teams promptly utilize OpenZeppelin’s tools (*https://defender.openzeppelin.com/v2/#/auth/sign-up*) to check if their contracts are vulnerable to the aforementioned exploit and take immediate actions such as pausing the project and revoking trust from forwarders , etc., to mitigate this risk.
This article is reprinted from AntChain Open Labs.
View the original article link: https://antchainopenlabs.github.io/2023/11/28/KyberSwap-Attack-Analysis-Unveiling-the-Most-Sophisticated-Cyber-Heist-in-History/
About
AntChain Open Labs
AntChain Open Labs is a research center initiated by AntChain and world leading computer scientists in the area of foundational trust technologies. It is dedicated to building a secure, transparent and reliable Web3 infrastructure driven by innovative research and aiming to advance transformative services. Website:*https://openlabs-intl.antdigital.com/home*
ZAN
ZAN, powered by AntChain Open Labs, provides solutions for Web3, such as Smart Contract Review, KYT, KYC, Node Service, and more. Website:*https://zan.top/home*
메타데이터
- post_id
- 993ee3cb30eb
- slug
- emergency-alert-analysis-of-openzeppelin-arbitrary-address-spoofing-attack-993ee3cb30eb
- url
- https://medium.com/@zan.top/emergency-alert-analysis-of-openzeppelin-arbitrary-address-spoofing-attack-993ee3cb30eb
- canonical_url
- https://medium.com/@zan.top/emergency-alert-analysis-of-openzeppelin-arbitrary-address-spoofing-attack-993ee3cb30eb
- author_url
- https://medium.com/@zan.top
- status
- ok
- fetched_at
- 2026-07-28 05:18:14