Main Image borrowed from rekt's report

Radiant Capital was hacked on Jan 2 just before 7 pm UTC for something close to 1900 ETH worth around $4.5 million. This is going to be a deep dive into how this happened.

The transaction that started it is available here if you want to follow along.

Overview

The hack consisted of three main steps:

  • Pumping of the liquidity index of the USDC reserve through flash loans,
  • Borrowing WETH against deposited funds,
  • Exploiting a rounding error to extract the original deposit

Step by step

The attack started with a 3 million USDC flash loan from AAVE. The attacker then deposited 2 million USDC into Radiant. Nothing fishy so far.

Step one: Inflating the liquidity index

The next thing is the first key component of the hack. The attacker took a 2 million USDC flash loan from Radiant, transferred 2 million USDC back into Radiant and withdrawn 2 million minus one-millionth USDC, leaving 1 USDC wei as the deposit.

component1.png The attacker withdraws 1999999999999 USDC wei

They then repaid the flash loan with a 1800 USDC fee. Repayment makes a call to ReserveLogic::cumulateToLiquidityIndex which makes the liquidity index explode.

component2.png New liquidity index computation

New liquidity index becomes (1800 * 1e6 + 1) * 1e27 = 1800000001000000000000000000000000000

component4.png New liquidity index in ReserveDataUpdated event

The clever thing here is that the attacker transferred the flash loaned USDC from Radiant back into the LendingPool to be able to withdraw all of it minus one wei and trigger the liquidity index bloat.

So the current state at this point is:

  • the attacker has close to 1 million USDC (minus the deposited wei and the 1800 USDC flash loan fee)
  • the LendingPool contract has a little over 2 million USDC
  • total liquidity of the USDC reserve is still 1 USDC wei

The attacker loops flash loaning 2 million USDC and repaying 2 million + 1800 USDC causing further expansion of the liquidity index.

component5.png Second iteration of the liquidity index expansion

component6.png Third iteration of the liquidity index expansion

component7.png 150th (final) iteration of the liquidity index expansion

This concludes the first step of the attack. The state at this point is as follows:

  • the attacker still has close to 700k USDC from the 3 million USDC AAVE flash loan
  • the attacker paid over 270 000 USDC in flash loan fees
  • the LendingPool contract has around 2.27 million USDC
  • the initial 1 wei deposit of the attacker is now worth a little over 270 000 USDC due to paid fees

Step two: Borrowing against the inflated deposit

Since the attackers deposit is worth over $270k they're allowed to borrow against it. They borrow a bit over 90.69 WETH from the LendingPool which at the time of the attack was worth a little under $214k. They maxed out the LTV, which makes sense.

Step three: Exploiting the rounding error

The attacker now deposits a little over 543k USDC (543600000002 USDC wei which is exactly 2 * 271800000001, which will be important later).

Now the attacker repeatedly withdraws 407 700 USDC and deposits 271 800 USDC (+ 1 wei) back. This doesn't sound right, now does it? They deposited 543k, withdrawn 407k and deposited back 271k, so after the first withdraw and deposit they have only around 407k and there can be no third iteration, right? Wrong.

Due to the inflated liquidity index the attacker receives 2 wei of the USDC aToken when they deposit the initial 543k USDC. When they withdraw the 407 700 (which "happens to be" precisely 1.5 * 271 800) the rayDiv rounding causes them to only burn 1 wei of the aToken. When they deposit back the 271 800 they gain that 1 wei back.

I wrote a quick foundry test to make sure the math is correct here. component8.png A foundry test for the third step of the attack

component9.png Test output

The relevant piece of code can be found in the AToken::burn function. In the burn function, amountScaled (equal to 1 wei) is being burned and the original amount (407 700) of the _underlyingAsset (USDC) is being transferred to the attacker.

component10.png The annotated burn function

component11.png USDC being withdrawn

Next, the attacker deposits back 271 800 USDC (+ 1 wei). The added wei makes the rayDiv amount to 1 wei instead of zero. Depositing 271 800 USDC adds 1 wei of the USDC aToken back to the attacker and makes repeating the withdraw and re-deposit loop possible. The attacker needs to recover the 2 million USDC transferred into the contract in step one plus the 271k paid in flash loan fees plus the 543k deposited at the start of step three, for a total of around 2.8 million USDC. The recovery happens at a rate of 135 900 USDC (407k - 271k) per iteration. There are 18 iterations netting 2 446 200 USDC and a final withdraw of 369 200 USDC for a total of 2 815 400 USDC, so close to 2.8 million which was the initial ballpark.

Cleanup

The attacker swaps some of the stolen WETH to USDC to cover the 1500 USDC AAVE flash loan fee and repay the AAVE flash loan.

component12.png AAVE flash loan repayment transfer

component44.png AAVE flash loan closing event

Finally, the attacker unwraps the remaining WETH into native ETH.

componentlast.png Unwrap WETH into ETH

Post-hack state

  • the attacker got away with the borrowed 90+ WETH
  • the attacker recovered all deposited USDC and repaid the AAVE flash loan
  • Radiant has a 1 USDC wei deposit "worth" over $270k due to liquidity index bloat
  • the Radiant LendingPool contract has no USDC to cover the deposit so they essentially accumulated 90 WETH bad debt with no collateral to cover it

Rinse and repeat

Steps two and three of the hack are repeated in two following transactions (here and here) with larger amounts (906 WETH extraction each) for a total of over 1900 WETH stolen.

What this exploit means for DeFi security

This attack highlights a class of vulnerabilities specific to lending protocols: liquidity index manipulation through flash loans combined with precision rounding errors. The attacker did not break any cryptography or exploit an access control flaw. They used the protocol's own math against it.

For teams building DeFi protocols, the takeaways are direct:

  • Rounding in token accounting must be audited for edge cases with extreme parameter values. The rayDiv rounding behavior was correct under normal liquidity index ranges but exploitable once the index was artificially inflated.
  • Flash loan interactions with core state variables (like liquidity indices) need dedicated invariant testing. Standard unit tests would not catch this because the exploit requires a specific sequence across multiple transactions.
  • Minimum liquidity thresholds can prevent index manipulation. If the protocol enforced a minimum reserve liquidity above 1 wei, the index inflation would not have been possible at this scale.

For more on how we approach DeFi protocol security at Oligamy, see our work on multi-currency stablecoin architecture where dual oracle validation and collateral floor mechanisms address similar classes of financial protocol risk. If you are building or auditing a DeFi protocol, our blockchain development and blockchain validation teams can help.

Written by Krzysztof Cywiński, blockchain developer at Oligamy Software. Original article on Medium.

Frequently Asked Questions

Post tag

Blockchain
Software