zkLend Finance Incident Analysis
On Feb 12, 2025, the zkLend Finance protocol was exploited, resulting in a loss of approximately $9.5 million.
zkLend is a decentralized lending protocol that operates on the Starknet blockchain. It allows users to lend and borrow assets, similar to protocols like Aave or Compound.
The attack targeted the zkLend Market
contract (specifically the wstETH market), which is the main contract that enables users to lend and borrow assets.
Key information
Attacker address: https://voyager.online/contract/0x04d7191dc8eac499bac710dd368706e3ce76c9945da52535de770d06ce7d3b26
Vulnerable contract (zkLend Market): https://voyager.online/contract/0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05
Sample attack transaction: https://voyager.online/tx/0x596bb905f74b545ca5a2af39c5724d952e43ef9887af3f6fd603eebfcc9c2a
Exploit analysis
Looking at the attacker address, we can see that there were multiple transactions that occurred during the time of the attack.
To understand the attack, we need to first examine the transaction that calls runwithfirstdep
, as it appears to be the most important one.
In the runwithfirstdep
transaction, we can see multiple consecutive deposit
and withdraw
calls to the zkLend Market
contract. Let's examine the details of one deposit
call first.
It's clear that the amounts in the deposit
and withdraw
calls are not the same, indicating this is not a simple deposit and withdraw transaction. Here, we see that the attacker only deposited 4.069
wstETH but withdrew 6.103
wstETH. Therefore, we need to examine the code of the deposit
and withdraw
functions to understand the attack.
Walking through the code of the deposit
function, we can see that it takes the amount
of input token and mints a corresponding amount of zToken
. The same applies to the withdraw
function. However, why could the attacker burn more zToken
than they should? This leads us to examine the implementation of the burn
function in the zToken
contract.
From the implementation of the burn
function, we can see that the actual amount burned is scaledDownAmount
. Additionally, by examining the safe_math::sub()
function, we can determine that the formula for calculating scaledDownAmount
is amount * SCALE / accumulator
.
So, the easiest way to check this value is by checking these values from the attack transaction. And we have the following values:
amount:
6103946859077466029
(withdraw call)accumulator:
4069297906051644020000000000000000000000000000
SCALE:
1000000000000000000000000000
From these values, we can calculate the scaledDownAmount
as follows:
scaledDownAmount = amount * SCALE / accumulator
= 6103946859077466029 * 1000000000000000000000000000 / 4069297906051644020000000000000000000000000000
= 1
So now, it's clear that this value was carefully chosen by the attacker to bypass the check of scaled_down_amount.is_non_zero()
. The deposit
transaction before the runwithfirstdep
transaction was used to manipulate the result of the get_lending_accumulator()
function to make this value much larger and effectively make the scaled_down_amount
to be 1
.
Post-attack analysis
After the attack, the stolen funds were bridged from Starknet to the Ethereum blockchain. The attacker then attempted to launder the funds by depositing them into Railgun privacy pools but was prevented by the protocol's policies. The failed money laundering attempt emphasizes the importance of balancing privacy and transparency in the DeFi space.
Lessons learned
This incident highlights the importance of careful validation of input values in smart contracts, especially when dealing with complex and sensitive financial operations. By learning from incidents like this one, the DeFi community can build more resilient systems and better safeguard user funds.