Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

Lack of Automated State Update in RToken Transfers Leads to Inaccurate Payments

Summary

A design flaw exists in the RToken.sol contract where transfers do not account for the latest reserve state updates. This leads to inaccurate RToken payments when users or smart contracts attempt to transfer RTokens in amounts equivalent to specific share values. (Note: One of the practical issues relates to users transferring more RTokens to StabilityPool.sol than intended that I have reported separately. The excess amount of RTokens transferred could have been used/leveraged elsewhere.) The issue arises because the reserve liquidity index (which affects token scaling) is not updated before transfers, causing users to unknowingly send more RTokens than intended due to outdated scaling factors.

Vulnerability Details

In RToken.sol, the transfer() and transferFrom() functions perform scaling calculations based on the reserve’s liquidity index:

RToken.sol#L213

uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());

RToken.sol#L224

uint256 scaledAmount = amount.rayDiv(_liquidityIndex);

getNormalizedIncome() returns outdated values if the reserve state has not been updated before the transfer occurs whereas _liquidityIndex is always equal to the initialized WadRayMath.RAY. These two code lines have been suggested getting removed to avoid double scaling via the overridden _update() as I have separately reported. Nevertheless, the liquidity index changes dynamically due to borrowing, repayments, and yield accumulation (which is another bug not accounted for that I have reported separately) from the Curve vault, making precise RToken payments unreliable without first refreshing the index.

The liquidity index is meant to be only increasing. Hence, users transferring RTokens to match a specific share token amount may send more than necessary. The index is neither updated in transfer() and transferFrom() nor _update(). This causes unintended losses and inefficiencies for users.

For example, when user calling StabilityPool.deposit() to mint deTokens, lendingPool.updateState() isn't invoked prior to making rToken.safeTransferFrom(). Consequently, more RTokens will be transferred from msg.sender to Stability Pool than intended. There're other issues entailed in this function logic, but the emphasis here is to underscore the need of a current state update in the RToken transfer logic as there may be other places reliant on it:

StabilityPool.sol#L174-L184

function deposit(uint256 amount) external nonReentrant whenNotPaused validAmount(amount) {
_update();
rToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 deCRVUSDAmount = calculateDeCRVUSDAmount(amount);
deToken.mint(msg.sender, deCRVUSDAmount);
userDeposits[msg.sender] += amount;
_mintRAACRewards();
emit Deposit(msg.sender, amount, deCRVUSDAmount);
}

While LendingPool.updateState() being permissionless is at the user's discretion to manually invoke,

LendingPool.sol#L433-L439

/**
* @notice Updates the state of the lending pool
* @dev This function updates the reserve state, including liquidity and usage indices
*/
function updateState() external {
ReserveLibrary.updateReserveState(reserve, rateData);
}

it creates usability friction still, requiring users and smart contracts to know when and how often to update the state. And, ideally, this will have to be done atomically.

As a result, external smart contracts integrating with RTokens may experience unexpected overconsumption when attempting to exchange the transfer action for precisely equivalent amounts of share tokens or even use it as a form of exact payment for the equivalent amount of reserve tokens in other situations. In the latter case, while LendingPool.withdraw() updates the liquidity index before burning RTokens and may be arguably used to resolve the issue, this is irrelevant to RToken transfers, simply because withdraw() withdraws reserve assets to msg.sender, which is technically not feasible.

Impact

  • Incorrect RToken Payments: Users and contracts attempting to send a precise amount of reserve tokens or mint an exchanged amount of share (e.g. deTokens) through RTokens will over-transfer (i.e. overpay), resulting in excess token expenditure.

  • Smart Contract Integration Risks: External DeFi protocols that integrate RTokens may miscalculate payments, leading to automation failures and/or inefficiencies.

  • User Confusion and UX Issues: Users querying balanceOf() and assuming it accurately reflects available transferable RTokens may face unexpected deductions when transferring.

Tools Used

Manual

Recommendations

Consider the following code refactoring:

RToken.sol#L207-L226

function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
- uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
+ ILendingPool(_reservePool).updateState();
return super.transfer(recipient, scaledAmount);
}
function transferFrom(address sender, address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
- uint256 scaledAmount = amount.rayDiv(_liquidityIndex);
+ ILendingPool(_reservePool).updateState();
return super.transferFrom(sender, recipient, scaledAmount);
}

In the light of this, implementing the following helper function that reveals the equivalent amount in reserve tokens will be recommended too:

function getTransferInputAmount(uint256 targetAmount) public returns (uint256) {
ILendingPool(_reservePool).updateState(); // ✅ Force update before fetching index
return targetAmount.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::transfer and transferFrom double-scale amounts by dividing in both external functions and _update, causing users to transfer significantly less than intended

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::transfer and transferFrom double-scale amounts by dividing in both external functions and _update, causing users to transfer significantly less than intended

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.