Fraxlend is a lending platform that provides lending markets between a pair of assets. Each pair is an isolated market which allows anyone to lend and borrow tokens. Lenders deposit assets into the pair and receive yield-bearing fTokens.
Last November we found a high severity vulnerability in the protocol that would allow an attacker to completely steal a deposit to an empty pool.
From a lender's perspective, the protocol functions as an ERC-4626 vault. They can deposit assets to mint fTokens (shares), that can be burned to redeem the corresponding underlying assets.
The contract tracks the total assets and fTokens supply in the VaultAccount
struct, where amount
represents the total assets and shares
represents the fTokens supply.
VaultAccount public totalAsset;
struct VaultAccount {
uint128 amount;
uint128 shares;
}
The following is a simplified version of how the contract calculates how many fTokens to mint a user for their deposit amount. (Here is a link to the original)
function toShares(VaultAccount memory total, uint256 amount) internal pure returns (uint256 shares) {
shares = (amount * total.shares) / total.amount;
}
Assuming there is only one existing share (total.shares=1
), we can see that if the total.amount
(total assets deposited) is larger than the deposit amount
, the number of shares
minted to a user will round down to zero.
This would allow an attacker to steal a user’s deposit, if they can sufficiently increase total.amount
while keeping total.shares
equal to 1 wei.
Now onto the hard part, actually inflating this share ratio (total.amount
: total.shares
).
Lets try and apply the classic inflation attack which in this case would involve minting a single wei of fTokens, then directly transferring assets to the pair contract to inflate the total assets value.
if only it was that simple...
The classic attack will not work because the contract doesn’t track total assets using asset.balanceOf(address(this))
. Instead, it tracks the total assets through the VaultAccount
struct, which is only updated by the deposited/withdrawn amount during calls to deposit/withdraw. This means that simply transferring assets into the contract won't have an effect on the total.amount
field.
This method of tracking total assets internally is actually a common recommendation from security firms, as a means of preventing inflation attacks, since they do indeed prevent the classic inflation attack.
To inflate the share value, we need to somehow increase the value of total.amount
without increasing total.supply
.
Since we can’t do this by simply transferring tokens in, we have to find another way.
What if we could exploit the rounding direction upon deposits?
When depositing assets, the contract rounds down the number of shares to mint to the user (in favour of the protocol). While this is the correct rounding direction (the alternative would allow minting of free shares), this technically causes a minor degree of vault inflation upon every deposit- because the increase in total.supply
will be truncated, while the increase in total.assets
is not.
For example, consider the following initial state:
total.shares = 1
total.amount = 2
If we deposit 1 token, the toShares
function will return 0 shares (floor(1 * 1 / 2)). The new state is:
total.shares = 1
total.amount = 3
We’ve technically inflated the share value, because total.amount
has increased while total.shares
remains unchanged. While the magnitude of it may seem insignificant (this inflation can only steal a deposit of less than 3 wei), let’s repeat the process and see what happens:
We can now deposit 2 tokens, and toShares
will still return 0 shares. The new state is:
total.shares = 1
total.amount = 5
Now we can deposit 4 tokens, and toShares
will still return 0 shares. The new state is:
total.shares = 1
total.amount = 9
Now we can deposit 8 tokens, and toShares
will still return 0 shares. The new state is:
total.shares = 1
total.amount = 17
Notice something? Each deposit allows us to increase the deposit size exponentially following a pattern, where is the number of deposits. This means the deposit size doubles with each successive deposit, inflating the share value very significantly after a few iterations. This inflation method is referred to as a stealth donation.
With only 40 atomic iterations, we can inflate the share value to over 1e12 which is enough to steal a 1 million USDC deposit.
With 70 iterations, we can inflate the share value to over 1e21 which is enough to steal 1000 ETH.
To calculate the number of iterations (𝑛) required to steal 𝑥 assets, we can use the formula: . The deposit size of the deposit is equal to
This is what the attack path would look like:
Attacker deposits 1 wei of assets to an empty pair to mint 1 fToken
Attacker deposits some collateral to borrow the 1 wei of assets, then repays it at least one block later, with an extra 1 wei of interest. This is all to achieve the required initial state of total.shares
= 1 and total.amount
= 2
Now the attacker waits patiently, observing the mempool for a victim that wants to deposit into the pair
The victim sends a transaction to deposit assets into the pair
The attacker frontruns the victim by performing deposits to inflate the share value, such that the victim gets 0 shares and essentially donated the deposit to the attacker
Now the attacker can redeem their single share, profiting the full deposit amount of the victim
In the past, there had been multiple pools with initial deposits valued up to $100k USD, with this pool as an example. Via this attack, the entire $100k could have been stolen by a malicious actor.
At the time of reporting there were 10 empty pairs that were vulnerable to the attack.
Upon confirmation of the vulnerability, @samkazemian immediately mitigated it by making a small deposit to the 10 vulnerable pools, ensuring that the share value can no longer be inflated. Subsequently the FraxlendPairDeployer
contract was updated to include a small initial deposit after deploying new pool, fixing the vulnerability.
This was an elusive attack vector that was missed by a tier 1 audit and a Code4rena audit contest with 136 participants.
At Obsidian Audits we consistently uncover vulnerabilities that others miss. If you're serious about the security of your DeFi protocol, and want it to be audited by publicly proven researchers, reach out to us here.