88MPH Theft Of Unclaimed MPH Rewards Bugfix Review
Summary
On September 25, 2022, 0xSzeth reported a high severity bug in 88mph’s vesting03
smart contract via Immunefi. This bug allowed some malicious users to drain the vesting contract of unclaimed MPH rewards (88mph tokens).
While looking through the contract, the whitehat discovered that some users were able to steal most of the 88mph tokens generated from yield in the vesting03
contract by depositing an asset and withdrawing the vested 88mph tokens immediately. In other words, users were able to withdraw deposits before the maturity date, receiving yield that they were not entitled to.
This is because while depositing an asset, the vestRewardPerTokenPaid[vestID]
was not updated with the current rewardPerToken
.
The whitehat discovered that the address 0xe67d3b0BDfd1D853FBcE6C0898b464e332a67B18
actively exploited the contract by depositing ETH to the contract and withdrawing the vested tokens immediately:
First exploit:
- https://etherscan.io/tx/0x8c2541448077015e952077015337a4ede92e5f05bdb9daad80e3df9ef9e52621 (Deposit on Sep-23–2022 07:05:59 PM +UTC)
- https://etherscan.io/tx/0x830c08c3de094b68493c794b9390bf5561ccef1d77f2568451737b0410a5492e (Withdraw on Sep-23–2022 07:06:23 PM +UTC)
Second exploit:
- https://etherscan.io/tx/0xbf823a243dc2316ce476327dd8d37e18c1ae03607107b9fed76d594e39dace01 (Deposit on Sep-23–2022 07:07:47 PM +UTC)
- https://etherscan.io/tx/0xd762dba091c7c2ce087f3b72467912599290207a49b627a13a8d25686b7aa070 (Withdraw on Sep-23–2022 07:08:23 PM +UTC)
Thanks to the whitehat’s investigation and a quick fix from the project, further losses were prevented.
The project paid out a $21,000 bounty for 0xSzeth’s find.
Vulnerability Analysis
88mph’s functionality allows users to deposit their assets into the DInterest
contract, which mints the user an ERC721 token that contains information about the deposit amount and maturity of the deposit. It also makes an external call to the MPHMinter
contract to forward the call to the vesting03
contract, which mints another ERC721 token. This second token contains information regarding vesting status, and vesting03
further allows users to claim their reward in the form of 88mph tokens.
The vulnerability existed in the vesting03
contract in the createVestForDeposit()
function. This function is responsible for storing initial information regarding user deposit and minting the vestID
.
However, this function didn’t update the vestRewardPerTokenPaid[vestID]
with the current rewardPerToken
, which should actually be very low after an immediate deposit because not enough time has passed for the user to earn their full token reward. So, the _earned()
function in the withdraw()
function will calculate as though the user were eligible for the full rewardPerToken
from the amount that they deposited.
A flashloan is the most natural way to exploit this vulnerability.
Here are the steps to reproduce the attack:
- Flashloan WETH from Uniswap or AAVE.
- Call
deposit()
to theDInterest
contract to deposit WETH. - Call
withdraw()
to thevesting03
contract by supplying thevestID
obtained from depositing WETH. - Call
withdraw()
toDInterest
contract by supplying true as an argument, in order to withdraw the initial deposit before the maturity ends. - Swap 88mph tokens obtained from step 3 to Balancer or Uniswap.
- Pay back the initial flashloan and fee.
POC (Proof Of Concept)
This POC is written for readers to study and test in Forge.
The steps to use this POC are as follows:
- Install https://github.com/foundry-rs/foundry.
- Replace
Counter.sol
in the src folder withWithFlashloan.sol
. - Replace
Counter.t.sol
in the test folder withWithFlashloan.t.sol
. - Change the alchemy API in the
WithFlashloan.t.sol
to your own API. - Run
forge test — match-path test/WithFlashloan.t.sol -vvv
This POC will make a local fork at 15598022 which is before the first malicious transaction is executed, and it will show an attacker can steal around 3.66 ETH.
WithFlashloan.sol
:
WithFlashloan.t.sol
:
The initial deposit amount was calculated by simulating the deposit funds, using only 0.1 ETH and 1 month maturity, and then we calculated it by reversing this formula:
FullMath.mulDiv(accountBalance, rewardPerToken_ —
vestRewardPerTokenPaid[vestID],PRECISION)
into this:
From this calculation, we can determine the exact amount that we should deposit to drain most of the 88mph tokens in the vesting03
contract. Once we know the amount, we can continue to exploit the vesting03
contract, using the POC above.
Vulnerability Fix
88MPH fixed the vulnerability by updating the vestRewardPerTokenPaid[vestID]
with the current rewardPerToken
when a user makes an initial deposit to the vesting03
contract.
Acknowledgements
We would like to thank 0xSzeth for doing an amazing job and responsibly disclosing such an important bug. Big props also to the 88mph team who responded quickly to the report and patched it.
Thanks also to Omik of Immunefi for writing this bugfix review.
If you’d like to start bug hunting, we got you. Check out the Web3 Security Library, and start earning rewards on Immunefi — the leading bug bounty platform for web3 with the world’s biggest payouts.
And if you’re feeling good about your skillset and want to see if you will find bugs in the code, check out the bug bounty program from 88mph.