Revest Finance Vulnerabilities: More than Re-entrancy

Securing the Future of DeFi: Lessons Learned from Revest Finance's Vulnerabilities

Revest Finance Vulnerabilities: More than Re-entrancy

On March 27th, 2022, the staking DeFi project Revest Finance on Ethereum was attacked due to the ERC-1155 call-back mechanism, which caused roughly $2M worth of tokens (namely BLOCKS, ECO, LYXe, and RENA) to be stolen. We analyzed the attack in the first place, and tweeted our analysis on that night (UTC+8).

In fact, at the time of writing the Twitter, we still had some doubts about a function in the Revest TokenVault contract. We looked into the contract trying to understand its functionality. Later we found that it's another critical zero-day vulnerability, which can be exploited in a far more simple way and can cause the same huge losses (as the attack that has happened).

We then contacted the Revest Finance team immediately, and they responded quickly and proposed a workaround for the vulnerability. After confirming the vulnerability cannot be triggered, we decided to release this blog.

The following of this blog consists of three parts: the mechanism of the Revest Finance, the original re-entrancy attack, and the new zero-day vulnerability.

What's the Revest Finance FNFT

The Financial Non-Fungible Token (FNFT) of Revest Finance makes the trustless transfer of future rights to locked assets possible. The entry contract (Revest contract) provides three different interfaces to mint FNFT by locking underlying assets:

  • mintTimeLock: the underlying asset will be unlocked after a period of time.
  • mintValueLock: the underlying asset will be unlocked when its value rises above or falls below a prescribed value.
  • mintAddressLock: the underlying asset will be unlocked by a prescribed account.

The Revest contract connects the other three contracts to lock and unlock underlying assets.

  • FNFTHandler: inherited from the ERC-1155 token. It creates a new FNFT with the incrementing fnftId for every lock. The lock prescribes the total supply of the new FNFT at the creation. The FNFT can not be minted in any other way but can be burned for unlocking underlying assets.

  • LockManager: records the unlocking conditions for each lock when creating and decides if the lock can be unlocked when unlocking.

  • TokenVault: receives and sends the underlying assets and records the metadata for each FNFT, such as the value of a specified FNFT.

We take mintAddressLock as an example to illustrate the process of minting FNFTs.

Figure 1
Figure 2

The above two figures describe how a FNFT is created, minted, and burned. Specifically, user A locks 100 WETH into Revest Finance, creating the corresponding FNFT with fnftId as 1. Finally, it mints 100 1-FNFT to specified recipients with specified shares.

Note that, once the underlying asset is unlocked, then every 1-FNFT can be burned for receiving one (*1e18) WETH. As shown in Figure 2, user B withdraws 25 (* 1e18) WETH by burning 25 1-FNFT.

In addition, the Revest contract provides another interface, named depositAdditionalToFNFT, that incurs two vulnerabilities that will be discussed in the following.

We first use the following two figures to describe the normal usage of this function.

Figure 3
Figure 4

The function depositAdditionalToFNFT locks more underlying assets to an existing lock (specified by fnftId). Reasonably(Figure 3), it requires the specified quantity to be the same as the total supply of specified FNFT and then evenly distributes the added assets to each specified FNFT.

Otherwise(Figure 4), it creates a new lock with the latest fnftId, burns the specified quantities of old FNFT and mints the specified quantity of new FNFT, and then records the new lock's depositAmount as the sum of the old lock's depositAmount and the specified amount, as shown in the following code.

// Now, we transfer to the token vault
if(fnft.asset != address(0)){
    IERC20(fnft.asset).safeTransferFrom(_msgSender(), vault, quantity * amount);
}

ITokenVault(vault).handleMultipleDeposits(fnftId, newFNFTId, fnft.depositAmount + amount);

emit FNFTAddionalDeposited(_msgSender(), newFNFTId, quantity, amount);

Since depositAmount recorded in the TokenVault contract indicates the amount of the underlying asset one specified FNFT can withdraw, that operation transfers the value of the specified quantities of old FNFT from the old lock to the new lock.

(Specified quantity greater than the total supply will revert the transaction)

What's the Re-entrancy Vulnerability

In this part, we will illustrate how the re-entrancy attack works and discuss the root cause and the fix method.

Figure 5
Figure 6
Figure 7

The above three figures basically describe the whole process of the re-entrancy attack. Specifically, the attacker first locks zero RENA token to mint 2 1-FNFT that has no value. Second, the attacker locks zero RENA token again but mints 360,000 2-FNFT that also has no value (now). During the last step, the attacker re-enters the Revest contract's depositAdditionalToFNFT function via the FNFTHandler's call-back mechanism inherited from the ERC-1155 token standard, which over-writes the depositAmount of the lock with fnftId as 2 before updating of fnftId. As a result, the attacker obtains 360,001 2-FNFT with the depositAmount as 1e18, which means he can withdraw 360,001 * 1e18 RENA from the TokenVault contract. Besides, the only cost is 1e18 RENA.

Fix Method

The codes of Revest Finance are completely in line with the classic re-entrancy pattern: use fnftId -> external call with callback mechanism -> update fnftId. Therefore, the most straightforward way to fix the issues is to break the pattern. The fixed code is shown below:

function mint(
    address account, 
    uint id, 
    uint amount, 
    bytes memory data
) external override onlyRevestController {
    require(amount > 0, "Invalid amount");
    require(supply[id] == 0, "Repeated mint for the same FNFT");
    supply[id] += amount;
    fnftsCreated += 1;
    _mint(account, id, amount, data);
}

First, it moves the update operation before the external call (_mint), which can avoid the attack. Second, since the system does not allow mint zero FNFT and repeated mint the same FNFT, it adds two checks to ensure the system works as expected, which can improve the system's safety.

The New Zero-day Vulnerability

When analyzing the code of Revest Finance, the function handleMultipleDeposits in the TokenVault contract always confuses us, the code of which is shown below.

function handleMultipleDeposits(
    uint fnftId,
    uint newFNFTId,
    uint amount
) external override onlyRevestController {
    require(amount >= fnfts[fnftId].depositAmount, 'E003');
    IRevest.FNFTConfig storage config = fnfts[fnftId];
    config.depositAmount = amount;
    mapFNFTToToken(fnftId, config);
    if(newFNFTId != 0) {
        mapFNFTToToken(newFNFTId, config);
    }
}

During the call to the depositAdditionalToFNFT function, the handleMultipleDeposits function changes depositAmount of the old lock or records it of the new one. When the newFNFTId is zero, it does not record the depositAmount of the new lock, because this is an operation to add additional assets to the existing lock.

According to common sense, when the newFNFTId is not zero, it only records depositAmount of the new lock but does not change depositAmount of the old one. However, the code tell us that it not only records depositAmount of the new lock but changes depositAmount of the old one.

We believe that is a serious zero-day logic vulnerability and then write a PoC to verify that. The following three figures describe how the PoC works.

Figure 8
Figure 9
Figure 10

Specifically, the attacker first locks zero RENA to mint 360,000 1-FNFT. After that, the attacker directly invokes the depositAdditionalToFNFT function to create a new lock. Due to the logic bug, the TokenVault contract incorrectly changes the depositAmount of the old lock from zero to 1e18. As a result, the attacker gains 359,999 1-FNFT worth of 359,999 RENA. Obviously, the PoC is far more simple than the real re-entrancy attack.

The Workaround to Fix the Vulnerability

This is a logic bug, and we recommend using the following code to fix it.

function handleMultipleDeposits(
    uint fnftId,
    uint newFNFTId,
    uint amount
) external override onlyRevestController {
    require(amount >= fnfts[fnftId].depositAmount, 'E003');
    IRevest.FNFTConfig memory config = fnfts[fnftId];
    config.depositAmount = amount;
    if(newFNFTId != 0) {
        mapFNFTToToken(newFNFTId, config);
    } else {
        mapFNFTToToken(fnftId, config);
    }
}

Since the two vulnerable contracts: TokenVault and FNFTHandler store a lot of critical states, the project can not re-deploy the TokenVault contract and the FNFTHandler contract without migrating states. To avoid the further attack to this vulnerability, the project re-deployed a lite version of Revest contract, which disables more complex functions to reduce the surfaces available to any would-be attacker. After checking the workaround, we believe that the lite Revest contract can mitigate the possible attacks mentioned in this blog.

Takeaway

Making a DeFi project secure is not an easy job. Besides the code audit, we think the community should take a proactive method to monitor the project status, and block the attack before it even takes place.

About BlockSec

BlockSec is a pioneering blockchain security company established in 2021 by a group of globally distinguished security experts. The company is committed to enhancing security and usability for the emerging Web3 world in order to facilitate its mass adoption. To this end, BlockSec provides smart contract and EVM chain security auditing services, the Phalcon platform for security development and blocking threats proactively, the MetaSleuth platform for fund tracking and investigation, and MetaDock extension for web3 builders surfing efficiently in the crypto world.

To date, the company has served over 300 esteemed clients such as MetaMask, Uniswap Foundation, Compound, Forta, and PancakeSwap, and received tens of millions of US dollars in two rounds of financing from preeminent investors, including Matrix Partners, Vitalbridge Capital, and Fenbushi Capital.

Official website: https://blocksec.com/

Official Twitter account: https://twitter.com/BlockSecTeam

Sign up for the latest updates