This content has been previously presented on Blockchain Security Level Up 0x00 (https://www.meetup.com/blockchain-security-level-up/events/284040437/) by Mr. Natsasit Jirathammanuwat, Smart Contract Auditor at Inspex
The slides can be downloaded here: https://public.inspex.co/slides/How_to_Avoid_a_Lucky_Hacker_in_the_NFT_Games.pdf
NFT games (GameFi) have become popular since the end of last year, and the popularity is still growing as more projects are being developed and launched. One thing that most GameFi projects have in common is the use of NFTs with multi-level rarities for in-game mechanisms. However, insecure minting mechanisms can result in overlooked risks to the GameFi platforms.
In the NFT minting process, the NFT’s metadata can be generated in 3 methods as follows:
- Set NFT URI to the generated NFT’s metadata on the off-chain components e.g., API endpoint, IPFS
- Assign the NFT’s metadata to the NFT smart contract by executing the contract’s mutation functions from off-chain
- Calculate the NFT’s metadata on-chain during the minting being executed
In this article, we will focus on the 3rd method, which is using the on-chain calculation to determine NFT’s metadata. This method is one of the most transparent ways of defining NFT metadata, but there are pitfalls to be aware of.
Commonly Found NFT Minting Mechanism
Inspex has implemented a hacking lab named Gacha Lab (https://gachalab.inspex.co/) as examples of vulnerable implementations in order to help security researchers or developers to understand the bad randomness issue for NFT minting mechanism.
The Gacha Lab main contracts are as follows:
ERC20token that is used for minting
ERC721token (NFT) which has the star state as a rarity of NFT
GachaMachine: a contract containing the random algorithm (
_random()) and NFT minting function (
GachaMachine contract source code shows the
roll() function handles the minting process, and it includes collecting the minting cost (
GachaTicket token), calling the random function, grading NFT rarity, and minting the NFT. The
_random() function handles the random algorithm which calculates the keccak256 hash of
msg.sender, and the total supply of the NFT, and use that hash as the result of the randomness.
Unlucky User Scenario
The following diagram shows the normal scenario of a user trying to mint an NFT.
When the user calls the
roll() function, the
GachaMachine contract will take a
GachaTicket token as the minting cost from the user. The
GachaMachine contract will then calculate the star and call the
mint() function to mint a
GachaCapsule token for the user. In the Gacha Lab Level 1 (https://gachalab.inspex.co/#/level1), you can actually execute the
GachaMachine contract and monitor the transaction to better understand them.
Lucky Hacker Presence
Here comes the “Lucky Hacker”, the person who does not want to waste his precious GachaTickets on junk rolls. When it comes to blockchain, there is one more thing to consider when writing a smart contract. The transaction in blockchain can be reverted, nullifying all changes done in the transaction as stated in the Solidity documentation (https://docs.soliditylang.org/en/v0.8.12/control-structures.html#error-handling-assert-require-revert-and-exceptions) as follows: “Solidity uses state-reverting exceptions to handle errors. Such an exception undoes all changes made to the state in the current call (and all its sub-calls) and flags an error to the caller.”
Using this ability, the “Lucky Hacker” can abuse the NFT minting mechanism by performing the following attack:
The lucky hacker deploys their
EvilContract contract and uses it to call the
roll() function of
GachaMachine contract. Then, they can perform additional checks on the rarity (
star) after the
GachaCapsule token is minted in
EvilContract. If they don’t gain the rarest one (
GachaCapsule.star != 5), they will be able to revert the transaction and get their
GachaTicket token back. To achieve the goal, they can perform this step repeatedly until getting the 5-star
GachaCapsule token. As a result, the “Lucky Hacker” always gains 5-star NFTs by using only 1
No More EvilContract
“Just prevent the function from being called by a contract”, said the poor developers who have just noticed that they got hacked. If the attacker needs to call the
roll() function from the smart contract to be lucky, let’s try to prevent this by determining whether the caller is a smart contract or not.
In the Gacha Lab Level 2 (https://gachalab.inspex.co/#/level2), the following
GachaMachine contract source code introduces the
!msg.sender.isContract() validation to the
Address.isContract() is Totally Inadequate
isContract() is typically used to check whether an address is a smart contract or not by validating the code size of that address; however, please keep in mind that this method can be bypassed.
Since the contract code is only stored at the end of the constructor execution, the lucky hacker can move his exploit logic to the constructor function in order to bypass this validation as shown in the following diagram:
EOA Check Wants to Join the Party
In the Gacha Lab Level 3 (https://gachalab.inspex.co/#/level3), the poor developer implements the EOA check (
tx.origin == msg.sender) to allow only EOA to call
roll() function. By utilizing this check, the lucky hacker will be unable to call the
roll() function via the
Not only the ability to revert the transaction that can be used to abuse this NFT minting mechanism. If the source of randomness is revealed or predicted, the random outcome can also be predicted!
The lucky hacker can implement the same random logic in his
CalculatorContract. Then, by using their deployed contract, they can predict the random result of the
roll() function. Finally, when the random result is satisfactory, they can execute the
roll() function on the same block to obtain the rarest NFT.
The main solution to resolve this issue is using a provably-fair and verifiable random function (VRF).
The verifiable random function is a random number generator that uses the private key and seed to generate the proof and the random value. Everyone can use the proof, seed, and the associated public key to verify that this value was calculated correctly. However, there is some problem to be concerned about when implementing the VRF on the blockchain.
Since there is no secret in blockchain, the private key must not be stored in the blockchain storage. Then the off-chain oracle will come to handle this thing. When the random function is called, the seed will be chosen by the smart contract and should not be controlled or predicted by the off-chain oracle e.g. future blockhash. The off-chain oracle will use the private key and the chosen seed to calculate the proof and random value then feed them back to the blockchain. The random result can be verified by providing the proof, seed, and the associated public key.
The simplest method of obtaining a secure random is using the existing VRF services such as:
- Chainlink VRF (Ethereum, BNB Smart Chain) (https://docs.chain.link/docs/chainlink-vrf/).
- Harmony VRF (Harmony One Chain) (https://docs.harmony.one/home/developers/tools/harmony-vrf)
To summarize, calculating the metadata of NFTs on-chain is one of the most transparent ways, but improper implementation allows attackers to abuse NFTs minting mechanism to gain the rarest NFT easily with low cost. The two main root causes are:
- The ability to revert the transaction if the outcome is unsatisfactory.
- Because there is no good resource of randomness for NFT minting, the random outcome will be predictable.
To solve this, the developer should implement a VRF in order to get a provably-fair and verifiable random value that cannot be predicted by anyone and will not be changed after being committed.
Inspex is formed by a team of cybersecurity experts highly experienced in various fields of cybersecurity. We provide blockchain and smart contract professional services at the highest quality to enhance the security of our clients and the overall blockchain ecosystem.