Summary
Rtoken is a rebasing token that increases the asset deposited value for each increase in index but when a user add to their deposits in the lending pool, the contract fails to mint this user the scaled balance. This implementation copied Aave v3 but this is not implemented correctly, hence the amount will reflect an amount higher than it should.
Vulnerability Details
When a user deposits or adds to his deposits
* @notice Allows a user to deposit reserve assets and receive RTokens
* @param amount The amount of reserve assets to deposit
*/
function deposit(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
ReserveLibrary.updateReserveState(reserve, rateData);
@audit>> uint256 mintedAmount = ReserveLibrary.deposit(reserve, rateData, amount, msg.sender);
_rebalanceLiquidity();
emit Deposit(msg.sender, amount, mintedAmount);
}
We transfer in funds and then get rtokens with them
function deposit(ReserveData storage reserve,ReserveRateData storage rateData,uint256 amount,address depositor) internal returns (uint256 amountMinted) {
if (amount < 1) revert InvalidAmount();
updateReserveInterests(reserve, rateData);
IERC20(reserve.reserveAssetAddress).safeTransferFrom(
msg.sender,
reserve.reserveRTokenAddress,
amount
);
@audit>> (bool isFirstMint, uint256 amountScaled, uint256 newTotalSupply, uint256 amountUnderlying) = IRToken(reserve.reserveRTokenAddress).mint(
address(this),
depositor,
amount,
reserve.liquidityIndex
);
amountMinted = amountScaled;
updateInterestRatesAndLiquidity(reserve, rateData, amount, 0);
emit Deposit(depositor, amount, amountMinted);
return amountMinted;
When obtaining rtoken the user is minted rtokens but this should be worth the amount transferred in and we should not just mint the token amount
* @notice Mints RToken to a user
* @param caller The address initiating the mint
* @param onBehalfOf The recipient of the minted tokens
* @param amountToMint The amount of tokens to mint (in underlying asset units)
* @param index The liquidity index at the time of minting
* @return A tuple containing:
* - bool: True if this is the first mint for the recipient, false otherwise
* - uint256: The amount of scaled tokens minted
* - uint256: The new total supply after minting
* - uint256: The amount of underlying tokens minted
*/
@audit>> 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;
@audit>> uint256 balanceIncrease = 0;
@audit>> if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
@audit>> balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
@audit>>update >> _userState[onBehalfOf].index = index.toUint128();
@audit>> we donot mint the scaled amount >> _mint(onBehalfOf, amountToMint.toUint128());
emit Mint(caller, onBehalfOf, amountToMint, index);
return (isFirstMint, amountToMint, totalSupply(), amountScaled);
}
LOOK at AAVE v3 atoken implantation => https://github.com/aave/aave-v3-core/blob/782f51917056a53a2c228701058a6c3fb233684a/contracts/protocol/tokenization/base/ScaledBalanceTokenBase.sol#L66-L88 .
function executeSupply(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
DataTypes.UserConfigurationMap storage userConfig,
DataTypes.ExecuteSupplyParams memory params
) external {
DataTypes.ReserveData storage reserve = reservesData[params.asset];
DataTypes.ReserveCache memory reserveCache = reserve.cache();
reserve.updateState(reserveCache);
ValidationLogic.validateSupply(reserveCache, reserve, params.amount);
reserve.updateInterestRates(reserveCache, params.asset, params.amount, 0);
IERC20(params.asset).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, params.amount);
@audit>> aave>> bool isFirstSupply = IAToken(reserveCache.aTokenAddress).mint(
msg.sender,
params.onBehalfOf,
params.amount,
reserveCache.nextLiquidityIndex
);
In the atoken contract =>
function mint(
address caller,
address onBehalfOf,
uint256 amount,
uint256 index
) external virtual override onlyPool returns (bool) {
return _mintScaled(caller, onBehalfOf, amount, index);
}
This calls the internal function
function _mintScaled(
address caller,
address onBehalfOf,
uint256 amount,
uint256 index
) internal returns (bool) {
@audit>> uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.INVALID_MINT_AMOUNT);
uint256 scaledBalance = super.balanceOf(onBehalfOf);
uint256 balanceIncrease = scaledBalance.rayMul(index) -
scaledBalance.rayMul(_userState[onBehalfOf].additionalData);
_userState[onBehalfOf].additionalData = index.toUint128();
@audit>> _mint(onBehalfOf, amountScaled.toUint128());
uint256 amountToMint = amount + balanceIncrease;
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(caller, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0);
}
We mint the scaled amount when liquidity is supplied in Aave the same was implemented for rtoken but we ended up minting the wrong amount to the user.
Impact
Users token balance tracking will be wrong and an attacker can use this to steal funds.
deposit at index =1e27 .
Alice deposits 1000 USD. if we query balance of we get 1000 rtokens
index now at 1.1 e27
Attacker deposits 1000 USD. if we query balance of we get 1000 rtokens again even with increased rate
minted 1000 rtokens
calls withdraw
Attackers balance equals 1100 USD instead of 1000 USD
Tools Used
Manual Review
Recommendations
Mint the scaled balance to the liquidity provider based on the current index as done by aave.