Due to a lack of comments or users' mistakes, users can mistakenly call functions that compute the loanRatio with incorrect token amount values, resulting in potential loss of funds.
When working with tokens that have different decimal precision, the current implementation exhibits inconsistent code which leads to financial losses and unintended behaviors.
The main issue arises from the absence of checks on client-provided input. These checks can be performed using the ERC20.decimals() function. Unfortunately, due to the custom version of the ERC20 interface used in the project, IERC20.decimals() is missing.
The way loanRatio is computed leads to contradictory expected inputs for functions that calculate it:
Given that token pairs in a pool can have varying decimals, the possible combinations are:
1ī¸âŖ Decimals Debt Token == Decimals Collateral Token
2ī¸âŖ Decimals Debt Token < Decimals Collateral Token
3ī¸âŖ Decimals Debt Token > Decimals Collateral Token
Given the way the loanRatio is computed and considering the above scenarios, the expected input values are:
(==): Both debt and collateral values multiplied by 10^ERC20.decimals().
(<): Desired debt value multiplied by 10^(debt.decimals) * 10^(Difference in Decimals). And collateral value multiplied by 10^(collateral.decimals).
(>): Desired debt value multiplied by 10^(debt.decimals). And collateral by 10^(collateral.decimals) * 10^(Difference in Decimals).
However, these calculations can lead to significant issues, particularly in scenarios 2 and 3. In them if the user sends the correct adjusted values so the loanRatio computation makes sense, later, the quanities of tokens transfered won't be the proper ones as they are not readjusted in the code.
Check the borrow() function for example:
If debt token has 8 decimals and collateral token has 6 decimals.
Attack vectors: pools resembling scenarios 2 and 3. An example of a pool with valuable assets could be:
Debt : USDT (6 decimals) đĩ
Collateral : WBTC (8 decimals) đĩ
...or vice versa.
đ§ Note â ī¸: Short auction times can exacerbate the vulnerabilities explained along the report.
đ Notice âšī¸: Assume the bitcoin price is lower than its nowadays values, making this loan scenario more realistic. The point of this example does not reside exactly in using WBTC and USDT. Rather it resides in using 2 tokens with real value and different decimals.
The user intends to borrow 10 USDT and deposit 1 WBTC as collateral.
The loanRatio (before considering token decimals) is:
loanRatio = Debt / Collateral = 10 USDT / 1 WBTC = 10
As Solidity only handles integer values, we must adjust for decimals to avoid losing precision.
USDT: Has 6 decimals. Therefore, 10 USDT in its smallest unit is:
10 * 10^6 = 10,000,000
WBTC: Has 8 decimals. Therefore, 1 WBTC in its smallest unit is:
1 * 10^8 = 100,000,000
loanRatio calculationFor the loanRatio to be calculated correctly, the decimals of the debt and the collateral must match.
USDT: Already has 10^8 (from the previous step).
WBTC: Needs to be adjusted to match the 8 decimals of USDT. This means multiplying by:
10^(8 - 6) = 10^2 = 100
This gives collateral's vairable
a WBTC quantity of = 1 WBTC * 100 = 100 WBTC
The loanRatio calculation will now be accurate, since both the debt and collateral are represented in 10^8 units. However, when the user sends the WBTC as collateral, they won't be sending just 1 WBTC, but rather 100 WBTC!
If, for any reason, the user has approved the transfer of more tokens than intended (e.g., due to a generous approval or some unexpected behavior from some scammer leveraging the _beforeTokenTranser() of an ERC777 token compatible with ERC20), they could end up depositing much more collateral than necessary. This could lead to potential theft of their tokens or miscomputations.
đ§ Note â ī¸ : If WBTC were to be the debt, consequences would be the same but the borrower instead of owing 1 WBTC as expected, he would be owing 100 WBTC.
đ§ Caution â ī¸ : Always ensure that the token adjustments for decimals are clearly communicated to the user and accurately handled in the smart contract to avoid unexpected token transfers. More on this on the Recommendations section.
Create a new test file in the tests directory, then run the PoC and read the console logs to guide you through the PoC's code.
Run the PoC with:
Code đ:
đ Notice âšī¸: PoC stands for "Proof Of Concept", the minimum amount of code to prove your statement.
Core Concern đģ: The contract inconsistently manages token decimals, resulting in a misleading API prone to user errors. Rather than relying on users' vigilance, smart contracts should inherently reduce potential mistakes, especially with financial implications.
Risks đĨˇ: Beyond financial losses, scammers can exploit these inconsistencies.
In essence, this vulnerability is not just a potential drain on users' wallets but a strain on trust in the system.
Manual audit
Slither
The tests created for the PoC.
Objective: Simplify the API to accept intuitive values for debt and collateral. Then make the contract internally adjust decimals. Though this might increase gas costs, it reduces potential financial errors, enhancing the protocol's accessibility. Of course, users can still accidentally send more money than intended, but this time the confusion doesn't stem from the system's API.
To simplify the users' exeprience with the API you can:
1ī¸âŖ Refactor the Borrow struct by adding collateralAdjusted and debtAdjusted, calculated as value * 10^ERC20.decimals().
2ī¸âŖ Developer Notes: For clarity, prepend in the Borrow structure a comment about this:
3ī¸âŖ Apply a conversion ratio for a precise loanRatio computation and integrate checks to validate transfer amounts.
đ Notice âšī¸ These extra parameters in the Borrow struct are what enables the transfered amounts checks.
Due to the for-loops in the code, we will use private functions instead of modifiers
đ§ Note â ī¸: The functions shown are illustrative and untested.
New ratio computation
Quantities Checking
Adapt the IERC20 to use the decimals() method or, preferably use IERC20Metadata.sol from OpenZeppelin which provides the decimals() function.
đ§ Note â ī¸: Please, take another audit after fixing this issues.
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.