Learn how attackers can exploit reentrancy vulnerabilities in ERC-1155 implementations to drain vault contracts. This practical example demonstrates a real-world attack scenario.
Understanding the vulnerable contract
We’ll examine a simplified vault contract that demonstrates the reentrancy vulnerability. Here’s how it should work:
- Users create ETH-locked NFTs via the
create
function - These NFTs can be freely transferred between users
- Users unlock ETH using the
payEth
function - NFT holders can then
withdraw
ETH by burning their NFTs
Below is the vulnerable Vault contract implementation:
Exploiting the vulnerability
The vulnerability lies in the mint
function’s external call to IERC1155Receiver(to).onERC1155Received()
. This call occurs before updating the fnftsCreated
counter, creating a reentrancy opportunity.
The attack vector
The attacker exploits two key contract features:
- The
id_to_required_eth[nft_id]
mapping controls the ETH lock amount - The
nft_price[nft_id]
sets the price per individual NFT
Attack steps
1. Call create
with a large nftAmount
but small value
2. During the mint callback, reenter with a small nftAmount
but large value
3. This sets a high nft_price[nft_id]
for all NFTs
4. Withdraw to receive: total_nfts * high_price
in ETH
Detailed attack flow
Let’s break down the attack step by step:
- Initial creation
- Attacker calls
create(1000, 1 wei)
- Vault mints 1000 NFTs with ID =
k
(getNextId()
)
- Attacker calls
- Reentrancy attack
- During
onERC1155Received()
callback: - Attacker calls
create(1, 1 ether)
- Same
nft_id
(k
) is used (counter not updated) - Sets
nft_price[k] = 1 ether
- During
- Profit extraction
- Attacker unlocks with 1 ETH
- Withdraws all 1001 NFTs
- Receives 1001 ETH (1001 NFTs * 1 ETH price)
Attacker contract
Proof of concept
Below is the complete attack implementation with Wake testing framework:
Running this exploit successfully drains the vault:
Preventing the attack
Two key approaches can prevent this vulnerability:
- Checks-effects-interactions pattern
- Update state variables before making external calls
- This is the recommended approach
- Reentrancy guard
- Use OpenZeppelin’s ReentrancyGuard
- Adds a modifier to prevent reentrant calls
Here’s the fixed implementation:
Key takeaways
- ERC standards’ external calls can create unexpected reentrancy vectors
- State variables shared across contracts need careful handling
- Always update state before external calls
- Consider using reentrancy guards as an additional safety measure
Further reading
Explore our Reentrancy Examples Repository for more attack vectors: