### ThunderLoan.sol - Starting At The Top Ok, we're ready to dive into ThunderLoan.sol! There are many ways we can approach a code base, but let's just go top to bottom like we've been doing. <details> <summary>ThunderLoan.sol</summary> ```js // .edee... ..... .eeec. ..eee.. // .d*" """"*e..d*"""""**e..e*"" "*c.d"" ""*e. // z" "$ $"" *F **e. // z" "c d" *. "$. // .F " " 'F // d J% // 3 . e" // 4r e" . d" // $ .d" . .F z ..zeeeeed" // "*beeeP" P d e. $**"" " // "*b. Jbc. z*%e.. .$**eeeeP" // "*beee* "$$eeed" ^$$$"" " // '$$. .$$$c // "$$. e$$*$$c // "$$..$$P" '$$r // "$$$$" "$$. .d // z. .$$$" "$$. .dP" // ^*e e$$" "$$. .e$" // *b. .$$P" "$$. z$" // "$c e$$" "$$.z$*" // ^*e$$P" "$$$" // *$$ "$$r // '$$F .$$P // $$$ z$$" // 4$$ d$$b. // .$$% .$$*"*$$e. // e$$$*" z$$" "*$$e. // 4$$" d$P" "*$$e. // $P .d$$$c "*$$e.. // d$" z$$" *$b. "*$L // 4$" e$P" "*$c ^$$ // $" .d$" "$$. ^$r // dP z$$" ^*$e. "b // 4$ e$P "$$ " // J$F $$ // $$ .$F // 4$" $P" // $" dP Gilo94' // https://www.asciiart.eu/nature/lightning // ▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄ ▄ ▄ ▄▄ ▄ ▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ // ▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░░▌ ▐░▌▐░░░░░░░░░░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ // ▀▀▀▀█░█▀▀▀▀ ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌░▌ ▐░▌▐░█▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌ // ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ // ▐░▌ ▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌ // ▐░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ // ▐░▌ ▐░█▀▀▀▀▀▀▀█░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀█░█▀▀ // ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ // ▐░▌ ▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░▐░▌▐░█▄▄▄▄▄▄▄█░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ // ▐░▌ ▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░▌ ▐░░▌▐░░░░░░░░░░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░▌ // ▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ // // ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄ ▄ // ▐░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░▌ ▐░▌ // ▐░▌ ▐░█▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀█░▌▐░▌░▌ ▐░▌ // ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌▐░▌ ▐░▌ // ▐░▌ ▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░▌ ▐░▌ // ▐░▌ ▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌ ▐░▌ // ▐░▌ ▐░▌ ▐░▌▐░█▀▀▀▀▀▀▀█░▌▐░▌ ▐░▌ ▐░▌ // ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ // ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░▌▐░▌ ▐░▐░▌ // ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░▌ ▐░░▌ // ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀▀ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.20; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { AssetToken } from "./AssetToken.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { OracleUpgradeable } from "./OracleUpgradeable.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IFlashLoanReceiver } from "../interfaces/IFlashLoanReceiver.sol"; contract ThunderLoan is Initializable, OwnableUpgradeable, UUPSUpgradeable, OracleUpgradeable { error ThunderLoan__NotAllowedToken(IERC20 token); error ThunderLoan__CantBeZero(); error ThunderLoan__NotPaidBack(uint256 expectedEndingBalance, uint256 endingBalance); error ThunderLoan__NotEnoughTokenBalance(uint256 startingBalance, uint256 amount); error ThunderLoan__CallerIsNotContract(); error ThunderLoan__AlreadyAllowed(); error ThunderLoan__ExchangeRateCanOnlyIncrease(); error ThunderLoan__NotCurrentlyFlashLoaning(); error ThunderLoan__BadNewFee(); using SafeERC20 for IERC20; using Address for address; /*////////////////////////////////////////////////////////////// STATE VARIABLES //////////////////////////////////////////////////////////////*/ mapping(IERC20 => AssetToken) public s_tokenToAssetToken; // The fee in WEI, it should have 18 decimals. Each flash loan takes a flat fee of the token price. uint256 private s_feePrecision; uint256 private s_flashLoanFee; // 0.3% ETH fee mapping(IERC20 token => bool currentlyFlashLoaning) private s_currentlyFlashLoaning; /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ event Deposit(address indexed account, IERC20 indexed token, uint256 amount); event AllowedTokenSet(IERC20 indexed token, AssetToken indexed asset, bool allowed); event Redeemed( address indexed account, IERC20 indexed token, uint256 amountOfAssetToken, uint256 amountOfUnderlying ); event FlashLoan(address indexed receiverAddress, IERC20 indexed token, uint256 amount, uint256 fee, bytes params); /*////////////////////////////////////////////////////////////// MODIFIERS //////////////////////////////////////////////////////////////*/ modifier revertIfZero(uint256 amount) { if (amount == 0) { revert ThunderLoan__CantBeZero(); } _; } modifier revertIfNotAllowedToken(IERC20 token) { if (!isAllowedToken(token)) { revert ThunderLoan__NotAllowedToken(token); } _; } /*////////////////////////////////////////////////////////////// FUNCTIONS //////////////////////////////////////////////////////////////*/ /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ function initialize(address tswapAddress) external initializer { __Ownable_init(msg.sender); __UUPSUpgradeable_init(); __Oracle_init(tswapAddress); s_feePrecision = 1e18; s_flashLoanFee = 3e15; // 0.3% ETH fee } function deposit(IERC20 token, uint256 amount) external revertIfZero(amount) revertIfNotAllowedToken(token) { AssetToken assetToken = s_tokenToAssetToken[token]; uint256 exchangeRate = assetToken.getExchangeRate(); uint256 mintAmount = (amount * assetToken.EXCHANGE_RATE_PRECISION()) / exchangeRate; emit Deposit(msg.sender, token, amount); assetToken.mint(msg.sender, mintAmount); uint256 calculatedFee = getCalculatedFee(token, amount); assetToken.updateExchangeRate(calculatedFee); token.safeTransferFrom(msg.sender, address(assetToken), amount); } /// @notice Withdraws the underlying token from the asset token /// @param token The token they want to withdraw from /// @param amountOfAssetToken The amount of the underlying they want to withdraw function redeem( IERC20 token, uint256 amountOfAssetToken ) external revertIfZero(amountOfAssetToken) revertIfNotAllowedToken(token) { AssetToken assetToken = s_tokenToAssetToken[token]; uint256 exchangeRate = assetToken.getExchangeRate(); if (amountOfAssetToken == type(uint256).max) { amountOfAssetToken = assetToken.balanceOf(msg.sender); } uint256 amountUnderlying = (amountOfAssetToken * exchangeRate) / assetToken.EXCHANGE_RATE_PRECISION(); emit Redeemed(msg.sender, token, amountOfAssetToken, amountUnderlying); assetToken.burn(msg.sender, amountOfAssetToken); assetToken.transferUnderlyingTo(msg.sender, amountUnderlying); } function flashloan( address receiverAddress, IERC20 token, uint256 amount, bytes calldata params ) external revertIfZero(amount) revertIfNotAllowedToken(token) { AssetToken assetToken = s_tokenToAssetToken[token]; uint256 startingBalance = IERC20(token).balanceOf(address(assetToken)); if (amount > startingBalance) { revert ThunderLoan__NotEnoughTokenBalance(startingBalance, amount); } if (receiverAddress.code.length == 0) { revert ThunderLoan__CallerIsNotContract(); } uint256 fee = getCalculatedFee(token, amount); // slither-disable-next-line reentrancy-vulnerabilities-2 reentrancy-vulnerabilities-3 assetToken.updateExchangeRate(fee); emit FlashLoan(receiverAddress, token, amount, fee, params); s_currentlyFlashLoaning[token] = true; assetToken.transferUnderlyingTo(receiverAddress, amount); // slither-disable-next-line unused-return reentrancy-vulnerabilities-2 receiverAddress.functionCall( abi.encodeCall( IFlashLoanReceiver.executeOperation, ( address(token), amount, fee, msg.sender, // initiator params ) ) ); uint256 endingBalance = token.balanceOf(address(assetToken)); if (endingBalance < startingBalance + fee) { revert ThunderLoan__NotPaidBack(startingBalance + fee, endingBalance); } s_currentlyFlashLoaning[token] = false; } function repay(IERC20 token, uint256 amount) public { if (!s_currentlyFlashLoaning[token]) { revert ThunderLoan__NotCurrentlyFlashLoaning(); } AssetToken assetToken = s_tokenToAssetToken[token]; token.safeTransferFrom(msg.sender, address(assetToken), amount); } function setAllowedToken(IERC20 token, bool allowed) external onlyOwner returns (AssetToken) { if (allowed) { if (address(s_tokenToAssetToken[token]) != address(0)) { revert ThunderLoan__AlreadyAllowed(); } string memory name = string.concat("ThunderLoan ", IERC20Metadata(address(token)).name()); string memory symbol = string.concat("tl", IERC20Metadata(address(token)).symbol()); AssetToken assetToken = new AssetToken(address(this), token, name, symbol); s_tokenToAssetToken[token] = assetToken; emit AllowedTokenSet(token, assetToken, allowed); return assetToken; } else { AssetToken assetToken = s_tokenToAssetToken[token]; delete s_tokenToAssetToken[token]; emit AllowedTokenSet(token, assetToken, allowed); return assetToken; } } function getCalculatedFee(IERC20 token, uint256 amount) public view returns (uint256 fee) { //slither-disable-next-line divide-before-multiply uint256 valueOfBorrowedToken = (amount * getPriceInWeth(address(token))) / s_feePrecision; //slither-disable-next-line divide-before-multiply fee = (valueOfBorrowedToken * s_flashLoanFee) / s_feePrecision; } function updateFlashLoanFee(uint256 newFee) external onlyOwner { if (newFee > s_feePrecision) { revert ThunderLoan__BadNewFee(); } s_flashLoanFee = newFee; } function isAllowedToken(IERC20 token) public view returns (bool) { return address(s_tokenToAssetToken[token]) != address(0); } function getAssetFromToken(IERC20 token) public view returns (AssetToken) { return s_tokenToAssetToken[token]; } function isCurrentlyFlashLoaning(IERC20 token) public view returns (bool) { return s_currentlyFlashLoaning[token]; } function getFee() external view returns (uint256) { return s_flashLoanFee; } function getFeePrecision() external view returns (uint256) { return s_feePrecision; } function _authorizeUpgrade(address newImplementation) internal override onlyOwner { } } ``` </details> > **Remember:** The intent here is a first pass, we're not going to try to go too deep right away. Often it's important to prime your brain with content before getting deep into review. ### Imports ```js import { AssetToken } from "./AssetToken.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { OracleUpgradeable } from "./OracleUpgradeable.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IFlashLoanReceiver } from "../interfaces/IFlashLoanReceiver.sol"; ``` These look pretty standard though a few imports may stand out or be unfamiliar so far such as [**OwnableUpgradeable**](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/access/OwnableUpgradeable.sol) which serves as an upgradeable variation of the famous ownable library. We may also want to gain more familiarity with: - [**UUPSUpgradeable**](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/proxy/utils/UUPSUpgradeable.sol) - UUPS proxy patter covered in the [**Foundry Full Course**](https://updraft.cyfrin.io/courses/advanced-foundry). A very common smart contract proxy pattern. - [**Address**](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol) - Library to simplify the handling of Address functionality. I recommend taking the time to read up on these in more detail. No issues with imports so far, let's keep going with the contract. ### State Variables State variables are always a great place to focus briefly in a review. Understanding the data a contract deems important enough to store can add a great deal of context. We may want to add notes further defining what these things mean/do. ```js // @Audit-Explainer: Seems to map AssetToken to it's underlying token eg. USDC -> USDCAssetToken mapping(IERC20 => AssetToken) public s_tokenToAssetToken; // The fee in WEI, it should have 18 decimals. Each flash loan takes a flat fee of the token price. // @Audit-Informational: s_feePrecision never changes, should be constant or immutable. uint256 private s_feePrecision; // @Audit-Informational: s_flashLoanFee never changes, should be constant or immutable. uint256 private s_flashLoanFee; // 0.3% ETH fee // @Audit-Explainer: Seems to be a mapping to identify when a token is in the middle of a flash loan. mapping(IERC20 token => bool currentlyFlashLoaning) private s_currentlyFlashLoaning; ``` By taking the time to look through where and how these variables are being used in the contract, we're able to identify situations like the `informationals` above which would help save a protocol gas! > The events we don't have a lot of context for yet, so we're kind of going to skip them and return as we see them crop up in the code. ### Modifiers ```js modifier revertIfZero(uint256 amount) { if (amount == 0) { revert ThunderLoan__CantBeZero(); } _; } modifier revertIfNotAllowedToken(IERC20 token) { if (!isAllowedToken(token)) { revert ThunderLoan__NotAllowedToken(token); } _; } ``` Pretty standard, the syntax looks good, no obvious flaws but the call to the `isAllowedToken` function is interesting and nice to see. ```js function isAllowedToken(IERC20 token) public view returns (bool) { return address(s_tokenToAssetToken[token]) != address(0); } ``` This function concisely checks that a token has an `AssetToken` mapping and that it is not mapped to `address(0)`, returning `true` for an allowed token and `false` otherwise. A question the comes to mind might be: ```js // @Audit-Question: Is there a way to unset or change allowed tokens? ``` Following this line of thought would lead us to discovering the `setAllowedToken` function, but we'll come back to this later... ### Wrap Up We're making good progress in `ThunderLoan.sol`, but there's lots to go yet. Things seem pretty benign so far, but we haven't deal with any real function logic yet! Let's keep our momentum and hit the remaining functions in the next lesson! > **Note:** As we jump through the contract, you might notice it becomes difficult to track what you've covered, or how far you've reviewed. Don't hesitate to leave a note somewhere directly in the code base to come back to!
Patrick begins his review of ThunderLoan.sol with an assessment of imports.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Solidity Developer
Smart Contract SecurityDuration: 25min
Duration: 1h 18min
Duration: 35min
Duration: 2h 28min
Duration: 5h 03min
Duration: 5h 22min
Duration: 4h 33min
Duration: 2h 01min
Duration: 1h 40min
Testimonials
Read what our students have to say about this course.
Chainlink
Chainlink
Gustavo Gonzalez
Solutions Engineer at OpenZeppelin
Francesco Andreoli
Lead Devrel at Metamask
Albert Hu
DeForm Founding Engineer
Radek
Senior Developer Advocate at Ceramic
Boidushya
WalletConnect
Idris
Developer Relations Engineer at Axelar