Open howlbot-integration[bot] opened 4 months ago
hansfriese marked the issue as duplicate of #144
hansfriese marked the issue as satisfactory
Hi @hansfriese
I appreciate your quick judging!
I could not understand your decision not to judge this report as a primary issue but as a duplicate of #144.
My reasons for declaring this report as a primary issue:
state.riskConfig.borrowATokenCap
using multicall(State storage state, bytes[] calldata data)
function.increase in borrowAToken supply <= decrease in debtToken supply
The multicall(State storage state, bytes[] calldata data)
function is not able to uphold this invariant. Hence,
increase in borrowAToken supply could be > decrease in debtToken supply
The point is that this would happen when the users would use the multicall as intended, that is, for a usual (deposit + repay) pair actions instead of an unusual only deposit action.
My reasons for not selecting #144 as the primary issue:
To consolidate this point, please allow me to quote the relevant section from the SC session, Fall 2023:
The requisites of a full mark report are:
Identification and demonstration of the root cause
Identification and demonstration of the maximum achievable impact of the root cause
The second point clearly states maximum achievable impact of the root cause.
Furthermore, it is stated that:
A lack of identification of maximal impact is grounds for partial scoring or downgrading of the issue.
Wardens are incentivized to build high-quality reports so that their reports get marked as primary issues. They spend valuable time in generating the PoC, thinking about the correct mitigation, and other important components of a high-quality report. A report has several components but the most important of them are:
This report:
I would be grateful if you could look into this and mark this report as the primary issue. I would be more than happy to provide more valuable input.
Thanks
OK. I agree.
hansfriese marked the issue as not a duplicate
hansfriese marked the issue as selected for report
hansfriese marked the issue as primary issue
Lines of code
https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/Multicall.sol#L29 https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/Multicall.sol#L37
Vulnerability details
Overview
The
multicall(bytes[] calldata _data)
function in the Size.sol contract does not work as intended. The intention of themulticall(bytes[] calldata _data)
function is to allow users to access multiple functionalities of the Size protocol, such as a (deposit and repay) pair, by a single transaction to Size.sol:https://github.com/code-423n4/2024-06-size/blob/main/src/Size.sol#L142
The multicall function allows batch processing of multiple interactions with the protocol in a single transaction. This also allows users to take actions that would otherwise be denied due to deposit limits. One of these actions is a (deposit and repay) pair.
Let's say a credit-debt pair exists. Assume that the tenor of the debt is 1 year and the future value is 100ke6 USDC. Let's say the borrower decides to repay the loan just 1 day before the maturity ends. During this 1 year, the total supply of borrowAToken had increased so much that the total supply of borrowAToken was just 10e6 USDC worth below the cap (that is, just below the cap) at the time when the borrower decided to repay the loan.
To repay a loan, Size requires the user to have sufficient borrowAToken:
https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/actions/Repay.sol#L49
The user achieves this by first depositing the required amount of underlying borrow tokens (here, USDC) and then calling the
repay(RepayParams calldata params)
function:https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/DepositTokenLibrary.sol#L49
Now, in our case, when the borrower decides to deposit 100ke6 USDC (at max if it would have some existing borrowAToken), he would not be able to do so (as the cap would be hit by depositing just 10e6 USDC). The situation is that the tenor is about to end (1 day left) and the borrower is not able to repay, not because he does not have money, but because borrowAToken's total supply cap does not allow him to deposit enough USDC.
To mitigate this, Size provides the multicall function, that bypasses the deposit limit and allows users to carry out such actions (LOC-80 in Deposit.sol):
https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/actions/Deposit.sol#L80
Let's say a user uses the multicall function for a (deposit and repay) pair action. The multicall function checks for an invariant that restricts users from depositing more borrowATokens than required to repay the loan by restricting
https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/CapsLibrary.sol#L19
The problem is that this is the exact invariant that is broken. The PoC below explains how in detail.
Proof of Concept
I have provided a thorough step-by-step PoC to explain how the invariant is broken.
Let's continue with our previous example. Let's say the borrowAToken cap is 1Me6 and its current supply is 990ke6. Before calling the multicall function, the borrowAToken balances are:
Also, before calling the multicall function:
The borrower calls the multicall function, with (deposit and repay) action pair in Size.sol:
https://github.com/code-423n4/2024-06-size/blob/main/src/Size.sol#L142
To make things simple to understand, let's assume that this is the first repayment of a loan, that is, other credit-debt pairs exist but have sufficient time to mature.
The borrower has used the multicall to deposit 500ke6 USDC and would want to repay his debt completely (100ke6 USDC). The deposit action would be performed (as the deposit limit check is bypassed). After the deposit is over, the borrowAToken balances would be:
After the repayment is over, the borrowAToken balances are:
Also, after repayment:
Now, the above situation breaks the invariant:
The multicall flow should revert. However, if we look at the
multicall(State storage state, bytes[] calldata data)
function in Multicall.sol:https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/Multicall.sol#L26
LOC-29 sets the
borrowATokenSupplyBefore
variable toborrowAToken.balanceOf(Size_contract)
:LOC-37 sets the
borrowATokenSupplyAfter
variable toborrowAToken.balanceOf(Size_contract)
:As per our example:
Now,
validateBorrowATokenIncreaseLteDebtTokenDecrease()
is called at LOC-40:https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/CapsLibrary.sol#L19
In this function, the first
if()
statement at LOC-27 would be executed, as per our examplewhich would be false.
Therefore, the control flow does not move in the
if()
block, and the protocol assumes "the supply is below the cap: do not revert". As a result, the multicall function would not revert.The root cause of this issue is the use of
state.data.borrowAToken.balanceOf(address(this))
to set variablesborrowATokenSupplyBefore
andborrowATokenSupplyAfter
. Instead ofstate.data.borrowAToken.balanceOf(address(this))
,state.data.borrowAToken.totalSupply()
should be used. This would work as then:And, in the
if()
block ofvalidateBorrowATokenIncreaseLteDebtTokenDecrease()
function, we would have:which would be true. Thus, the control flow would enter into the
if()
block. At LOC-35, we would have:https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/CapsLibrary.sol#L35
that is:
which would be true again and finally, the transaction would revert.
This root cause gives rise to one more issue:- A user would use the multicall for a deposit action only but would be able to bypass the deposit limit. To comprehend this, think of a similar situation as above where:
Moreover, to keep things simple, let's assume that:
Now, a user deposits 200ke6 USDC using the multicall function. The normal (that is, without multicall) USDC deposit flow contains a logic to check for the deposit limit, which is bypassed when a multicall is used:
https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/actions/Deposit.sol#L80
After completing the deposit, the multicall function would call the
validateBorrowATokenIncreaseLteDebtTokenDecrease()
function for a check (similar to above). The following arguments would have the specified value:Now, again in the
if()
block at LOC-27:https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/CapsLibrary.sol#L27
we would have the following:
which would be false, and, so, the control flow would not enter the
if()
block. Thus the multicall flow would pass with the following result:The user was successful in depositing USDC in spite of the fact that his deposit crossed the deposit limit.
Severity
Impact: An invariant, which should not break, is broken. This point sets the impact of this issue to be Medium.
Likelihood: Any user would call the Multicall function (since it is not access-controlled) and can bypass the deposit limit as well as the restriction: increase in borrowAToken supply <= decrease in debtToken supply. This makes the likelihood high.
The final severity comes to be Medium. Moreover, the multicall functionality does not work as intended, affecting the availability of the correct intended version of multicall. This is in line with the most-used definition of Medium according to C4 docs:
https://docs.code4rena.com/awarding/judging-criteria/severity-categorization#estimating-risk
Tools Used
Manual review
Recommended Mitigation Steps
Apply the following in
multicall(State storage state, bytes[] calldata data)
function:https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/Multicall.sol#L26
Assessed type
Invalid Validation