Submitted by prapandey031, also found by Inspex, samuraii77, muellerberndt, 3n0ch, Jorgect, Honour, nnez, MidgarAudits, elhaj, alix40, 0xRobocop, 0xrafaelnicolau, mt030d, inzinko, rscodes, 0xpiken, hezze, evmboi32, zarkk01, trachev, KupiaSec, stakog, BoltzmannBrain, bin2chen, pkqs90, shaflow2, VAD37, Sentryx, ether_sky, said, and SpicyMeatball
https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/Multicall.sol#L29https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/Multicall.sol#L37
The multicall(bytes[] calldata _data) function in the Size.sol contract does not work as intended. The intention of the multicall(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.
Lets say a credit-debt pair exists. Assume that the tenor of the debt is 1 year and the future value is 100ke6 USDC. Lets 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 borrowATokens 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
Lets 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, this is the exact invariant that is broken. The PoC below explains how in detail.
I have provided a thorough step-by-step PoC to explain how the invariant is broken. Lets continue with our previous example.
Lets 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, lets 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 to borrowAToken.balanceOf(Size_contract):
LOC-37 sets the borrowATokenSupplyAfter variable to borrowAToken.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 example:
 which 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 variables borrowATokenSupplyBefore and borrowATokenSupplyAfter. Instead of state.data.borrowAToken.balanceOf(address(this)), state.data.borrowAToken.totalSupply() should be used. This would work as then:
And, in the if() block of validateBorrowATokenIncreaseLteDebtTokenDecrease() 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:
To comprehend this, think of a similar situation as above where:
Moreover, to keep things simple, lets 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.
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.
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
Invalid Validation
aviggiano (Size) confirmed and commented via duplicate Issue #144:
Note: For full discussion, see here.
