Summary
As this protocol allow owner of the protocol to set any token for lending and flashloaning. However not all token have 18 decimals like USDC which has only 6 decimals and WBTC which has 8 decimals.
Vulnerability Details
This protocol is minting a ERC20 token for every token deposited according to the exchangeRate
of that token but the protocol is not taking account for tokens which has less than 18 decimals like WBTC & USDC.
Let's take an example:
Bob deposited 2 USDC ie 2e6
According to protocol Bob should get 2 lpUSDC ie 2e18 at an exchangeRate
of 1:1 but
Bob is getting only 0.000000000002 lpUSDC ie 2e6 because protocol consider all token 18 decimals.
Similar is the case with ThunderLoan::redeem
, and ThunderLoan::getCalculateFee
//here is the POC and I've created a MockERC20 USDC which has 6 decimals, below is MockERC20 USDC code and test file.
Create this MockERC20 in mock test file and import it in baseTest.t.sol file and make a USDC token with 6 decimals just like you are making tokenA.
pragma solidity 0.8.20;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
uint8 _decimals;
constructor(uint8 __decimals) ERC20("ERC20Mock", "E20M") {
_decimals = __decimals;
}
function mint(address account, uint256 amount) external {
_mint(account, amount);
}
function burn(address account, uint256 amount) external {
_burn(account, amount);
}
function decimals() public view override returns (uint8) {
return _decimals;
}
}
This is the test case
pragma solidity 0.8.20;
import { Test, console } from "forge-std/Test.sol";
import { BaseTest, ThunderLoan } from "./BaseTest.t.sol";
import { AssetToken } from "../../src/protocol/AssetToken.sol";
import { ERC20Mock } from "@openzeppelin/contracts/mocks/ERC20Mock.sol";
import { MockFlashLoanReceiver } from "../mocks/MockFlashLoanReceiver.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
contract ThunderLoanTest is BaseTest {
uint256 constant AMOUNT = 10e18;
uint256 constant DEPOSIT_AMOUNT = AMOUNT * 100;
address liquidityProvider = address(123);
address user = address(456);
address user2 = address(789);
MockFlashLoanReceiver mockFlashLoanReceiver;
function setUp() public override {
super.setUp();
vm.prank(user);
mockFlashLoanReceiver = new MockFlashLoanReceiver(address(thunderLoan));
}
function test_tokenLessThan18Decimal_lose_funds() public {
vm.startPrank(thunderLoan.owner());
thunderLoan.setAllowedToken(USDC, true);
AssetToken assetUSDC = thunderLoan.getAssetFromToken(USDC);
uint256 amount = 2e6;
USDC.mint(user, amount);
vm.startPrank(user);
USDC.approve(address(thunderLoan), amount);
thunderLoan.deposit(USDC, amount);
console.log("userLpUSDC", assetUSDC.balanceOf(user));
assert(assetUSDC.balanceOf(user) == 2e18);
}
}
To run the test
forge test --mt test_tokenLessThan18Decimal_lose_funds -vvv
Here is the result
Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 2.50ms
Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/unit/MyTest.t.sol:ThunderLoanTest
[FAIL. Reason: Assertion violated] test_tokenLessThan18Decimal_lose_funds() (gas: 1196849)
Impact
LP will loose funds as well as interest in the form of exchangeRate
Tools Used
Manual review, Foundry
Recommendations
Add this to ThunderLoan::deposit
+ uint256 mintAmount = (
+ amount * 10 ** (18 - IERC20Metadata(address(token)).decimals()) * assetToken.EXCHANGE_RATE_PRECISION()
+ ) / exchangeRate;
Add this to ThunderLoan::redeem
+ uint256 amountUnderlying = (amountOfAssetToken * exchangeRate)
+ / (assetToken.EXCHANGE_RATE_PRECISION() * 10 ** (18 - IERC20Metadata(address(token)).decimals()));
Add this to ThunderLoan::getCalculatedFee
+ uint256 valueOfBorrowedToken = (
+ amount * 10 ** (18 - IERC20Metadata(address(token)).decimals()) * getPriceInWeth(address(token))
+ ) / s_feePrecision;