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.
In RToken.sol, the transfer()
and transferFrom()
functions perform scaling calculations based on the reserve’s liquidity index:
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:
While LendingPool.updateState()
being permissionless is at the user's discretion to manually invoke,
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.
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.
Manual
Consider the following code refactoring:
In the light of this, implementing the following helper function that reveals the equivalent amount in reserve tokens will be recommended too:
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.