Description
The protocol incorrectly mints RTokens during deposits by using the wrong amount in the mint function. Instead of minting the scaled amount amountToMint.rayDiv(index), it mints the raw deposit amount amountToMint. This results in users receiving more RTokens than they should. The scaling mechanism is essential because it accounts for the accumulation of interest in the protocol through the liquidity index. When users deposit, their deposit amount should be divided by the current liquidity index to determine how many RTokens they receive. However, the current implementation mints RTokens 1:1 with the deposit amount, ignoring this scaling factor, minting more RTokens than expected.
Context
Impact
High. When users deposit tokens, they receive more RToken than they should due to the incorrect amount being minted. This leads to users being able to withdraw more underlying tokens than they initially deposited, and potentially withdraw other users funds.
Likelihood
High. This issue occurs on every deposit transaction due to a core calculation error in the mint function, making it consistently exploitable by any user depositing into the protocol.
Proof of Concept
scenario 1:
Deposit at time t1:
initial liquidity index : 1.01
initial deposit amount : 2000 crvUSD
amount minted expected : 2000 / 1.01 = 1980.1980198 rToken
amount minted actual : 2000 rToken
Withdraw at time t2:
final liquidity index : 1.02
amount withdrawable expected : 1980.1980198 * 1.02 = 2019.8019802
amount withdrawable actual : 2000 * 1.02 = 2040
amount withdrawable excess : 2040 - 2019.8019802 = 20.1980198
scenario 2:
Deposit at time t1:
initial liquidity index : 1.01
initial deposit amount : 2000 crvUSD
amount minted expected : 2000 / 1.01 = 1980.1980198 rToken
amount minted actual : 2000 rToken
Withdraw at time t1:
final liquidity index : 1.01
amount withdrawable expected : 1980.1980198 * 1.01 = 2000
amount withdrawable actual : 2000 * 1.01 = 2020
Recommendation
function mint(
address caller,
address onBehalfOf,
uint256 amountToMint,
uint256 index
) external override onlyReservePool returns (bool, uint256, uint256, uint256) {
if (amountToMint == 0) {
return (false, 0, 0, 0);
}
uint256 amountScaled = amountToMint.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
uint256 balanceIncrease = 0;
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
_userState[onBehalfOf].index = index.toUint128();
- _mint(onBehalfOf, amountToMint.toUint128());
+ _mint(onBehalfOf, amountScaled);
emit Mint(caller, onBehalfOf, amountToMint, index);
- return (isFirstMint, amountToMint, totalSupply(), amountScaled);
+ return (isFirstMint, amountScaled, totalSupply(), amountToMint);
}