Uranium Finance Exploit - Technical Analysis

Originally published
April 14, 2021

We received warnings from the community who reported that the Uranium Finance project suffered an exploit. Uranium Finance published a post-mortem on Medium, but this fails to highlight the technical details regarding the exploit. Below, we dive deeper into the exploit through a technical analysis in order to uncover the vulnerability that lies in the contract alongside the workflow of the exploit.

The attacker's address:

https://bscscan.com/address/0x36ad9ee78bfb730955993d2aa77ecccf95e3313e

The exploited contract(MasterUranium):

https://bscscan.com/address/0xd5aac41d315c1d382dcf1c39d4ed9b37c224edf2

The vulnerable contract is a modified version of the yield farming MasterChief contract. The additional code implements the concept of "bonus reward" which introduces the vulnerability. The attack consists of the repeat of deposit(_pid, _amount), emergencyWithdraw(_pid), and withdraw(_pid, _amount) transactions. The attacker drains RADS/sRADS rewards tokens from the vulnerable contract and sells them for $1.3M worth of BUSD and BNB. 

1. In the first step, the attacker calls the deposit(_pid, _amount) function. 


When the deposit(_pid, _amount) function is called with the _amount input argument larger than "0", the "_bonusAmount" is calculated with 

_bonusAmount = _amount.mul(userBonus(_pid, _user).add(10000)).div(10000);

The userBonus(_pid, _user) equals 0 in the context of the code. Simplify the equation, we get

_bonusAmount=_amount

On the next line, the user.amountWithBonus adds the  _bonusAmount to become a larger value. At the end of the deposit function, the user.rewardDebt is calculated with 

user.rewardDebt = user.amountWithBonus.mul(pool.accRadsPerShare).div(1e12);

After the deposit function is called, two variables relevant to the exploit are user.amountWithBonus and user.rewardDebt, they both contain a value larger than 0. 

2. In the next step, the attacker calls the emergencyWithdraw(_pid) function


The purpose of the function call is

  1. Get the deposited token back
  2. Let user.amount equal to 0
  3. Let user.rewardDebt equal to 0

Recall two variables relevant to the exploit are the user.amountWithBonus and user.rewardDebt, now one of them, the user.rewardDebt equals 0 now while the other one user.amountWithBonus larger than 0.

user.rewardDebt = 0

user.amountWithBonus = x(x>0)

3. In the last step, the attacker calls the withdraw(_pid, _amount) function: 

The attacker calls the withdraw function with the _amount variable equal to 0. On mark #1, the require statement is satisfied because user.amount and _amount both equal to 0. On mark #2, pending is calculate with

pending = user.amountWithBonus.mul(pool.accRadsPerShare).div(1e12).sub(user.rewardDebt);   

From the last emergencyWithdraw function call, the user.rewardDebt equal to 0, the equation becomes 

pending = user.amountWithBonus.mul(pool.accRadsPerShare).div(1e12);   

Both pool.accRadsPerShare and user.amountWithBonus equal to a positive number, hence the product pending larger than 0 as well. Assume the _isSRadsRewards equal false, on mark#3, the contract transfers Rads token to the msg.sender which is the attacker. Because the input argument _amount equal to 0 which fails the if statement on mark #4, the code can't reach the line(mark#5) that adjusts the user.amountWithBonus variable to indicate the user claims the reward. 

The user.amountWithBonus increases every time the attacker calls the deposit function in each iteration of the attack. The variable eventually used to calculate the pending variable which indicates how much reward token the attacker can get. This enables the attacker to drains more and more tokens in the process.




Summary

In short, the attacker calls the deposit function to increase the value of user.amountWithBonus, then calls the emergencyWithdraw function to get his deposit back and set user.rewardDebt equal to 0, finally he calls the withdraw function to receive the RADS/sRADS rewards tokens. Repeat the process to drain most of the reward tokens from the pool. 

Robust security assessments are essential in the world of DeFi. Protocols hold assets contributed by the community, and project owners have an obligation to ensure that the protocol(s) underpinning their project are secure. CertiK offers security assessments utilizing best-in-class technologies conducted by experienced security professionals. Visit our Security Leaderboard at https://www.certik.org to access security insights of projects and stay alert to what's happening in the space.



Reference

The attacker's address

https://bscscan.com/address/0x36ad9ee78bfb730955993d2aa77ecccf95e3313e

Contract got exploited(MasterUranium)

https://bscscan.com/address/0xd5aac41d315c1d382dcf1c39d4ed9b37c224edf2

Uranium’s Post-Mortem

https://uraniumfinance.medium.com/uranium-post-mortem-v2-compensations-aac4b0706d7d

Transactions - The attack

https://bscscan.com/tokentxns?a=0x36ad9ee78bfb730955993d2aa77ecccf95e3313e&p=2

Transactions - deposit

https://bscscan.com/tx/0x09976b55015997df711be8f911afe6db2f21b40728532f16ee96257b4a52a48f

Transactions - emergencyWithdraw

https://bscscan.com/tx/0x730ad83dd0aa96519a8876ef28f26620ad6a4ca7a614d2aca661b51e874c6c07

Transactions - withdraw

https://bscscan.com/tx/0xb9b7005fcf0b05161c5db136092372c743e74b48ecf7e85e588a84fee777ffcf