On November 16th, 2022, the adidas Web3 Studio announced their new NFT collection, adidas Virtual Gear, that expands their “Into the Metaverse” narrative. However, the mint process wasn’t as smooth as expected: NFTs stolen, unboxing paused, hours of uncertainty, and more. This article will explore how I found the critical vulnerability, how it worked, how easy it was to exploit, the response by the team, and a solution. Let’s dive in!
It all began on December 16, 2021, when adidas Originals announced their intent for web3 with Into the Metaverse (ITM), an exclusive NFT drop in collaboration with Bored Ape Yacht Club, PUNKS Comic, and gmoney (known as Phase 1).
This Phase 1 NFT allowed its holder to burn it to redeem physical merch and mint the Phase 2 NFT. After this, the adidas team airdropped a new NFT to the Phase 2 holders known as the Meta Capsule, needed to unlock a Virtual Gear.
The Virtual Gear collection consists of ERC721s that represent virtual clothing items. To obtain one, the user needed to burn its Meta Capsule, a process introduced in the Virtual Gear contracts through the burnToMint function. The mint process of a Virtual Gear looked like this:
Go to the adidas Metaverse site.
Connect your wallet and prepare to mint Virtual Gear (need to be holding a Meta Capsule — all are the same, yet this is an ERC721).
As the two NFTs involved were separate contracts with separate logic, the user needed to approve the new contract (Virtual Gear) the ability to transfer the Meta Capsule to burn it. Hence, the user needed to send two transactions:
Second transaction calls the burnToMint function in the Virtual Gear contracts, which makes the contract burn the Meta Capsule and mint a Virtual Gear to the caller (msg.sender) 👀.
Discovering the vulnerability
This process seems fairly normal. However, there is a fatal vulnerability in the Virtual Gear contract.
I was looking to burn my Meta Capsule and mint it in exchange for the Virtual Gear NFT. Out of curiosity, I checked the Virtual Gear contract and explored the functions I was supposed to call after approving the contract to transfer my Meta Capsule.
After a glance, I noticed the code was held to a very low standard: poor documentation with almost no comments or function notes. Weird, I thought, considering this was a pretty big drop with more than 20,000 users involved and an NFT collection with a multi-million market cap. This sparked my curiosity, so I took a closer look into the
burnToMint function and quickly realized the absence of security checks. The function only verified that the mint started, but not the supply count and that the transaction's sender is the valid owner of the given Meta Capsule in the input data. This latter mistake was the fatal error that allowed anyone to call the
burnToMint given any Meta Capsule approved to be burned as input.
How can this be exploited? Take a look:
An exploiter creates a program that listens to all the Meta Capsule approval events giving the Virtual Gear contract permission to transfer the Meta Capsule.
With a list of approved Meta Capsules, the explorer front runs users’ second transaction by calling the
burnToMintfunction with the list as input.
burnToMint function doesn’t check that the caller is the current Meta Capsule owner. Even worse, the approval in the first transaction was
setApprovalForAll, and the
burnToMint function accepted an array of Meta Capsule NFT IDs. This means that users that held more than one Meta Capsule and gave approval for all to the Virtual Gear contract were at risk of losing all of their Meta Capsules as the exploiter could use as input an array of the approved Meta Capsules.
As soon as I ran a Tenderly simulation and realized ALL Meta Capsule holders are at risk, I opened a ticket in the adidas discord. It took them an hour to raise my ticket to the studio (quite slow, considering the significance of the vulnerability). I believe the staff on the discord are not the technical devs; hence they had to communicate the problem to a possibly outsourced team (which delayed the whole process).
Another aspect to emphasize is how uncertain were the explanations made by the staff when facing the public. One example is the announcement below, which states that the vulnerability only affected 0.1% of the Meta Capsule holders (when it’s actually 100% of users trying to mint the Virtual Gear NFT).
The solution in the announcement is also not effective; they decided to modify the first transaction to use
setApproval and not
setApprovalForAll, meaning users approved Meta Capsules individually instead. This doesn't solve anything, as an exploiter only needs to change the type of event it's listening to and can continue to steal NFTs.
After realizing this change wasn't secure enough, they finally stopped the mint (5hrs and 35mins after I opened my ticket). After a paused day, the team returned with a new (better) Virtual Gear contract and migrated all of the NFTs to the new contract. There is, however, a much simpler and cost-effective solution.
Instead of creating a whole new NFT contract and paying huge gas fees, it is easier to deploy a multicall contract that aggregates the two transactions needed to burn and mint the NFTs. Users would interact with this contract as an intermediary, which executes the approval and mint functions in one transaction preventing the attack. In technical detail, the contract would work like this:
User transfer one or more Meta Capsules to the contract.
The contract takes note of the received Meta Capsules and assigns them to the
msg.sender(owner) in a mapping.
The user calls a function in the contract which executes the burn and mint process atomically:
Contract retrieves the Meta Capsules saved by the user and gives full approval to the Virtual Gear contract.
Contract calls the
burnToMintfunction and receives the respective Virtual Gear NFTs.
Contract transfers the Virtual Gear NFTs back to the user.
Users would still run a risk when doing the process manually (as it creates two separate transactions).
This post was written to demonstrate that NFT contracts can easily have vulnerabilities, despite using secure and common codebases like OpenZeppelin, while also pointing out the importance of auditing any smart contract.