# Foundry DeFi Stablecoin CodeHawks Audit Contest - Findings Report
# Table of contents
- ## [Contest Summary](#contest-summary)
- ## [Results Summary](#results-summary)
- ## High Risk Findings
- ### [H-01. `getTokenAmountFromUsd()` doesn't consider different token and oracle decimals](#H-01)
- ### [H-02. Ensure `DSCEngine` is the owner of `DecentralizedStableCoin`](#H-02)
- ## Medium Risk Findings
- ### [M-01. Chainlink staleness threshold should be set per asset](#M-01)
- ### [M-02. Precision loss may occur when calculating health factor](#M-02)
- ### [M-03. Use `safeTransfer` for token transfer](#M-03)
# <a id='contest-summary'></a>Contest Summary
### Sponsor: Cyfrin
### Dates: Jul 24th, 2023 - Aug 5th, 2023
[See more contest details here](https://www.codehawks.com/contests/cljx3b9390009liqwuedkn0m0)
# <a id='results-summary'></a>Results Summary
### Number of findings:
- High: 2
- Medium: 3
- Low: 0
# High Risk Findings
## <a id='H-01'></a>H-01. `getTokenAmountFromUsd()` doesn't consider different token and oracle decimals
### Relevant GitHub Links
https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/blob/main/src/DSCEngine.sol#L347
## Summary
`getTokenAmountFromUSD()` returns an incorrect amount for tokens with decimals != 18. It also returns an incorrect amount if chainlink oracle uses a different decimal than 8.
## Vulnerability Details
`getTokenAmountFromUSD()` calculates the final amount as follows:
```solidity
return ((usdAmountInWei * PRECISION) / (uint256(price) * ADDITIONAL_FEED_PRECISION));
```
This is fine for current collaterals: WETH and WBTC. However, if new collateral is added in future with different decimals, the protocol will return incorrect amount to liquidator. Here is the derivation of correct formula:
- Let `C` be chainlink oracle decimal.
- Let `D` be token decimal.
- `price/1eC` USD gets 1eD tokens.
- `(price/1eC) * (usdAmountInWei/1e18)` USD gets `1eD * (usdAmountInWei/1e18)` tokens.
- `(usdAmountInWei/1e18)` USD gets:
`usdAmountInWei * 1eD * 1eC / (price * 1e18)` which is the final formula to be used in `getTokenAmountFromUsd()`.
`C` and `D` can be fetched by calling `decimals()` on the collateral token and chainlink oracle. To save gas, you can also store these values permanently.
## Impact
For tokens and chainlink oracle deviating from the 18 and 8 decimals respectively, protocol will work incorrectly.
## Tools Used
Manual review.
## Recommendations
Update the return value of [`getTokenAmountFromUsd()`](https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/blob/main/src/DSCEngine.sol#L347) to consider token decimals and chainlink decimals.
## <a id='H-02'></a>H-02. Ensure `DSCEngine` is the owner of `DecentralizedStableCoin`
### Relevant GitHub Links
https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/blob/d1c5501aa79320ca0aeaa73f47f0dbc88c7b77e2/src/DSCEngine.sol#L112
## Summary
Any address can be passed as `dscAddress` to `DSCEngine`'s constructor. So there is no guarantee that `dscAddress` is the intended stablecoin, or if `DSCEngine` is the owner of `dscAddress`. In both these cases, the protocol won't work.
## Vulnerability Details
If `DSCEngine` is not the owner of `dscAddress`, then the mint and burn calls will revert as they are guarded by `onlyOwner` modifier.
## Impact
Protocol will break, and no one can mint or burn DSC tokens.
## Tools Used
Manual review.
## Recommendations
Update `DSCEngine's` constructor to deploy `DSCEngine`. This ensures that the correct contract is deployed at `dscAddress` and the ownership is also set correctly:
```solidity
constructor(address[] memory tokenAddresses, address[] memory priceFeedAddresses) {
// USD Price Feeds
if (tokenAddresses.length != priceFeedAddresses.length) {
revert DSCEngine__TokenAddressesAndPriceFeedAddressesMustBeSameLength();
}
// For example ETH / USD, BTC / USD, MKR / USD, etc
for (uint256 i = 0; i < tokenAddresses.length; i++) {
s_priceFeeds[tokenAddresses[i]] = priceFeedAddresses[i];
s_collateralTokens.push(tokenAddresses[i]);
}
i_dsc = new DecentralizedStableCoin();
}
```
# Medium Risk Findings
## <a id='M-01'></a>M-01. Chainlink staleness threshold should be set per asset
### Relevant GitHub Links
https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/blob/main/src/libraries/OracleLib.sol#L19
## Summary
Every chainlink oracle comes with its own `heartbeat` which denotes the maximum time gap between 2 price updates. Chainlink price feeds can go stale, so the stablecoin protocol has a staleness check but the threshold is set globally and doesn't account for different oracles.
## Vulnerability Details
This protocol sets the staleness check threshold to 3 hours:
```solidity
uint256 private constant TIMEOUT = 3 hours; // 3 * 60 * 60 = 10800 seconds
```
However, for some assets this can be too long or too short. For example [ETH/USD oracle](https://data.chain.link/ethereum/mainnet/crypto-usd/eth-usd) has a heartbeat of 1 hour, and stablecoin oracles usually have a heartbeat of 24 hours.
## Impact
Liquidation may occur on stale prices harming the protocol and its users.
## Tools Used
Manual review.
## Recommendations
Create a mapping to store a timeout for each collateral asset. At the time of fetching price for an asset, use the associated timeout for staleness check.
## <a id='M-02'></a>M-02. Precision loss may occur when calculating health factor
### Relevant GitHub Links
https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/blob/d1c5501aa79320ca0aeaa73f47f0dbc88c7b77e2/src/DSCEngine.sol#L330
## Summary
`collateralAdjustedForThreshold` calculation can lose precision when `collateralValueInUsd * LIQUIDATION_THRESHOLD` is not exactly divisible by `LIQUIDATION_PRECISION`. The resulting value would roundoff to the lowest value and would lead to precision loss since decimals are not handled in Solidity.
## Vulnerability Details
```solidity
uint256 collateralAdjustedForThreshold = (collateralValueInUsd * LIQUIDATION_THRESHOLD) / LIQUIDATION_PRECISION;
return (collateralAdjustedForThreshold * 1e18) / totalDscMinted;
```
For example, for `collateralValueInUsd = 267`, `collateralAdjustedForThreshold = 133` when the actual value should be `133.5`.
## Impact
Calculated health factor will be lower than the real value.
## Tools Used
Manual review
## Recommendations
We multiply `1e18` with `collateralAdjustedForThreshold`. Instead, first multiply and calculate the entire numerator before dividing:
```diff
+return (collateralValueInUsd * LIQUIDATION_THRESHOLD * 1e18) / (totalDscMinted * LIQUIDATION_PRECISION);
-uint256 collateralAdjustedForThreshold = (collateralValueInUsd * LIQUIDATION_THRESHOLD) / LIQUIDATION_PRECISION;
-return (collateralAdjustedForThreshold * 1e18) / totalDscMinted;
```
## <a id='M-03'></a>M-03. Use `safeTransfer` for token transfer
### Relevant GitHub Links
https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/blob/d1c5501aa79320ca0aeaa73f47f0dbc88c7b77e2/src/DSCEngine.sol#L157
https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/blob/d1c5501aa79320ca0aeaa73f47f0dbc88c7b77e2/src/DSCEngine.sol#L274
https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/blob/d1c5501aa79320ca0aeaa73f47f0dbc88c7b77e2/src/DSCEngine.sol#L287
## Summary
If non-compliant ERC20 tokens are added in future as collateral (like USDT), then protocol may not be able to handle the transfers.
## Vulnerability Details
The protocol use `transfer` and `transferFrom` to transfer collateral token. It also verifies that its return value is `true`:
```solidity
bool success = IERC20(tokenCollateralAddress).transferFrom(msg.sender, address(this), amountCollateral);
if (!success) {
revert DSCEngine__TransferFailed();
}
```
For WETH and WBTC, if transfer doesn't revert, `success` is always true. Hence this check can be skipped to save gas. However, if we want to add other collateral tokens in future, it's recommended to use OpenZeppelin's `SafeERC20` library which accommodates non-compliant ERC20 tokens. For example, USDT which doesn't return anything on a successful transfer.
## Impact
## Tools Used
Manual review.
## Recommendations
Use OpenZeppelin's [`SafeERC20`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol#L36-L44) library to handle ERC20 transfer. Add the following at the top of the contract:
```solidity
using SafeERC20 for IERC20;
```
Now replace `.transfer` and `.transferFrom` with `.safeTransfer` and `.safeTransferFrom`. For instance:
```diff
+IERC20(tokenCollateralAddress).safeTransferFrom(msg.sender, address(this), amountCollateral);
-bool success = IERC20(tokenCollateralAddress).transferFrom(msg.sender, address(this), amountCollateral);
-if (!success) {
- revert DSCEngine__TransferFailed();
-}
```