Summary
The RToken contract applies normalized income scaling twice during transfers, dividing by the index value twice (in transfer and _update ) instead of once, resulting in reduced transfer amounts. The reduction becomes more severe as the index grows over time.
Vulnerability Details
The transfer
function scales the transfer amount
by dividing by the liquidity index.
function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
return super.transfer(recipient, scaledAmount);
}
However, _update
also scales the already scaled amount, resulting in double scaling of the original transfer amount.
function _update(address from, address to, uint256 amount) internal override {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}
Impact
The actual transfer amount reduces as the index grows
Proof of concept
Copy the test to ./test/unit/core/tokens/RToken.test.js
Then run npx hardhat test ./test/unit/core/tokens/RToken.test.js
it.only("Incorrect transfer amount", async function () {
const index = ethers.parseUnits("2", 27);
mockLendingPool.mockGetNormalizedIncome(index);
const mintAmount = ethers.parseEther("100");
await mockLendingPool.mockMint(reservePool.address, user1.address, mintAmount, index);
const transferAmount = ethers.parseEther("50");
await rToken.connect(user1).transfer(user2.address, transferAmount);
const scaledBalanceUser1 = await rToken.balanceOf(user1.address);
const scaledBalanceUser2 = await rToken.balanceOf(user2.address);
expect(scaledBalanceUser1).to.equal(mintAmount - transferAmount / 2n);
expect(scaledBalanceUser2).to.equal(transferAmount / 2n);
});
});
Tools Used
Manual
Vs code
Recommendations
The transfer amount should only be scaled once, in _update