Summary
The mint function mints an insufficient amount of tokens by not accounting for accrued interest (balanceIncrease) when creating new tokens. This violates the protocol's interest accrual mechanism and results in users receiving fewer tokens than entitled.
Vulnerability Details
In the current implementation, when new tokens are minted, the contract calculates two components:
amountToMint: The new deposit amount
balanceIncrease: Accrued interest based on the user's existing balance and index change
However, the contract only mints amountToMint, ignoring the balanceIncrease:
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/RToken.sol#L136
_mint(onBehalfOf, amountToMint.toUint128());
The correct implementation should be
_mint(onBehalfOf, (amountToMint + balanceIncrease).toUint128());
Impact
Users lose accrued interest on existing balances
Protocol accounting becomes inaccurate
Violates ERC-20 token standard expectations
Proof of concept
Scenario 1: Single User Interest Loss
// Initial setup
- Initial index: 1.0 RAY (1e27)
- User deposits: 1000 tokens
- New index: 1.1 RAY
// Calculations
scaledBalance = 1000
oldIndex = 1.0 RAY
newIndex = 1.1 RAY
balanceIncrease = 1000 * (1.1 - 1.0) = 100 tokens
// Current behavior
Minted amount = 1000 tokens
// Correct behavior
Should mint = 1000 + 100 = 1100 tokens
// Result
User loses 100 tokens of accrued interest
Scenario 2: Compound Interest Loss
// Step 1
- Initial deposit: 1000 tokens
- Index increases: 1.0 → 1.1
- Interest earned but not minted: 100 tokens
// Step 2
- Additional deposit: 500 tokens
- Index increases: 1.1 → 1.2
- New interest earned but not minted:
((1000 + 500) * (1.2 - 1.1)) = 150 tokens
// Total loss
- First interest loss: 100 tokens
- Second interest loss: 150 tokens
- Total: 250 tokens
Recommendations
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();
uint256 totalMintAmount = amountToMint + balanceIncrease;
_mint(onBehalfOf, totalMintAmount.toUint128());
emit Mint(caller, onBehalfOf, totalMintAmount, index);
emit Transfer(address(0), onBehalfOf, totalMintAmount);
return (isFirstMint, totalMintAmount, totalSupply(), amountScaled);
}