An attacker can drain the protocol by repeatedly depositing and redeeming tokens.
Exchange rate is updated on deposit, although no user fees have been received. This
results in a deposit immediately followed by redeem to return more tokens than was
deposited.
Deposit/redeem cycles can be done repeatedly until the protocol is fully drained.
The protocol is designed to allow depositors to deposit capital and earn interest
from flashloaners. The mechanism is that depositors deposit a protocol-approved
ERC20 token and in return get a protocol controlled AssetToken that represents
their deposit.
The AssetToken accrues interest on the underlying capital based on loaner activity.
Users, or flash loaners, pay fees which are the foundation of the interest
that the liquidity providers earn.
After any amount of time, a liquidity provider can redeem his asset tokens, and
get the original underlying token back, plus the fees it has generated.
The interest mechanism is implemented using a variable called
exchangeRate which is used to convert between asset tokens that the liquidity
providers hold and the underlying tokens that the protocol holds.
For deposits, given an amount tokenAmount
of ERC20 tokens deposited, the number of asset tokens, assetTokenAmount that
the liquidity provider
receives back is given by:
Conversely, when redeeming a specified number of asset tokens, the liquidity
provider will receive:
The exchangeRate is increased whenever a user takes out a loan,
and in that way all the liquidity providers will get a greater
amount of underlying tokens back and be able to earn interest from loaners.
The problem in this case, though, is that exchangeRate is also increased whenever a liquidity provider makes a deposit. This enables an attack through deposit immediately
followed by a redeem. Following is high level flow of such an attack outlined.
(We assume for simplicity that the initial exchange rate is 1.0 and leave
out the decimal precision details of calculations.)
Attacker deposits 10 WETH
ThunderLoan will calculate and mint a number of asset tokens.
The formula above with the values substituted gives
assetTokenAmount = tokenAmount / exchangeRate = 10 / 1 = 10
The exchange rate is then updated. We'll skip the exact calculations - the
important thing to note is that exchange rate will now be slightly larger,
let's say 1.01 for the sake of illustrating the concept.
Attacker redeems the 10 asset tokens
ThunderLoan will calculate number of underlying WETH to return for the
redeemed asset tokens: amountWeth = assetTokens * exchangeRate = 10 * 1.01 = 10.1.
In this case we get the returned WETH amount = 10 * 1.01 = 10.1 WETH.
End result: The attacker has put in 10 WETH and got out 10.1 WETH. The protocol
and its remaining depositors have lost 0.1 WETH.
These steps can be repeated as many times as the attacker would like, and the whole
protocol will be thus be drained.
The relevant logic of the flow above is part of the files:
src/protocol/ThunderLoan.sol
src/protocol/AssetToken.sol
In particular the following parts are of interest:
the exchange rate update logic in deposit: https://github.com/Cyfrin/2023-11-Thunder-Loan/blob/8539c83865eb0d6149e4d70f37a35d9e72ac7404/src/protocol/ThunderLoan.sol#L153-L154
usage of exchange rate when redeeming tokens in redeem: https://github.com/Cyfrin/2023-11-Thunder-Loan/blob/8539c83865eb0d6149e4d70f37a35d9e72ac7404/src/protocol/ThunderLoan.sol#L174
the updating of the exchange rate in updateExchangeRate, called by deposit: https://github.com/Cyfrin/2023-11-Thunder-Loan/blob/8539c83865eb0d6149e4d70f37a35d9e72ac7404/src/protocol/AssetToken.sol#L80-L96
This test function, added to the test contract in test/unit/ThunderLoanTest.t.sol is
a proof of concept of how the protocol can be attacked using the outline described above.
Note that this test passes, but the log output will give details of the problem.
After adding the test, running the command
forge test --mt RedeemLoop -vvv
yields the following output.
As can be seen, the attacker has increased his
token amount from 500000000000000000000 to 501504507515510528411.
The protocol's balance has decreased by the same delta.
This illustrates the idea; taken further by either
repeating more times or increasing the capital used, the protocol can be drained completely.
Note that even an attacker without a massive bankroll could easily get hold of a large
amount of initial capital through a flashloan from a competing lending platform (or
possibly from ThunderLoan itself should ThunderLoan have missed some reentrancy
check somewhere).
Altering the number of loops run in inside the test to 100 iterations gives
the output (snipped), roughly a 10% gain on the initial capital:
With enough iterations (around 667 or so based on the other simulation parameters) the attacker can completely drain the protocol.
High severity - all protocol funds will be drained.
Manual review
Remove the exchange rate update logic in the deposit() function of src/protocol/ThunderLoan.sol, as shown in this diff:
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.