Summary
In ThunderLoan.sol:153
, there is an absence of proper access control mechanisms to verify if the entity attempting to repay the loan is the actual borrower. This poses a security risk as it allows for unauthorized repayment, potentially leading to incorrect state changes or other unintended interactions.
Vulnerability Details
Vulnerable code:
function repay(IERC20 token, uint256 amount) public {
if (!s_currentlyFlashLoaning[token]) {
revert ThunderLoan__NotCurrentlyFlashLoaning();
}
AssetToken assetToken = s_tokenToAssetToken[IERC20(token)];
token.safeTransferFrom(msg.sender, address(assetToken), amount);
}
Impact
Without strict checks on who can call the repay
function, there's a risk that anyone could interfere with the repayment process, potentially allowing for malicious entities to disrupt the normal flow of the smart contract. This could lead to loss of funds, denial of service, or unintended contract states which might be exploited.
Tools Used
Manual Review
Vs Code
Recommendations
Implement Access Control: The contract should check if msg.sender
is the borrower recorded in the contract's state before allowing the repay
function to proceed. This can be implemented using mapping to keep track of each borrower's state and require statements to assert proper authorization.
Possible fix:
Add a mapping to track activeBorrowers:
mapping(IERC20 => address) private activeBorrowers;
Add borrower to the mapping when initiating a flashloan:
function flashloan(address receiverAddress, IERC20 token, uint256 amount, bytes calldata params) external {
uint256 startingBalance = IERC20(token).balanceOf(address(assetToken));
if (amount > startingBalance) {
revert ThunderLoan__NotEnoughTokenBalance(startingBalance, amount);
}
if (!receiverAddress.isContract()) {
revert ThunderLoan__CallerIsNotContract();
}
activeBorrowers[token] = receiverAddress;
uint256 fee = getCalculatedFee(token, amount);
assetToken.updateExchangeRate(fee);
emit FlashLoan(receiverAddress, token, amount, fee, params);
s_currentlyFlashLoaning[token] = true;
assetToken.transferUnderlyingTo(receiverAddress, amount);
receiverAddress.functionCall(
abi.encodeWithSignature(
"executeOperation(address,uint256,uint256,address,bytes)",
address(token),
amount,
fee,
msg.sender,
params
)
);
uint256 endingBalance = token.balanceOf(address(assetToken));
if (endingBalance < startingBalance + fee) {
revert ThunderLoan__NotPaidBack(startingBalance + fee, endingBalance);
}
s_currentlyFlashLoaning[token] = false;
}
Verify caller is the actual borrower and reset state after repaying the flashloan:
function repay(IERC20 token, uint256 amount) public {
if (!s_currentlyFlashLoaning[token]) {
revert ThunderLoan__NotCurrentlyFlashLoaning();
}
address borrower = activeBorrowers[token];
require(msg.sender == borrower, "Caller is not the borrower");
AssetToken assetToken = s_tokenToAssetToken[IERC20(token)];
token.safeTransferFrom(msg.sender, address(assetToken), amount);
delete activeBorrowers[token];
}