Summary
The transfer() and transferFrom() functions scale the amount using rayDiv with liquidityIndex, which is correct. However, _update() also applies rayDiv, causing the scaling to be performed twice, resulting in users receiving less than intended.
Vulnerability Details
This issue exist in transfer and transferFrom But i will only focus on transfer function.
The transfer() apply rayDiv before calling super.transfer().
/contracts/core/tokens/RToken.sol:213
213: function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
214: uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
215: return super.transfer(recipient, scaledAmount);
216: }
_update() also applies rayDiv, effectively reducing the transfer amount.
/contracts/core/tokens/RToken.sol:308
308: function _update(address from, address to, uint256 amount) internal override {
309:
310: uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
311: super._update(from, to, scaledAmount);
312: }
This issue will effect the receiver in case when liquidityIndex is not 1e27.
POC
Add Following test case on rToken.test.js in describe section Basic functionality. run with command npx hardhat test .
This test case will revert as the amount minted and transfered are not same which is issue.
it.only("should allow transfers between users", async function () {
const mintAmount = ethers.parseEther("100");
const index = RAY;
await mockLendingPool.mockMint(reservePool.address, user1.address, mintAmount, index);
await mockLendingPool.mockGetNormalizedIncome(ethers.parseUnits("2", 27));
let currentIndex = await mockLendingPool.getNormalizedIncome();
console.log("currentIndex" , currentIndex);
const transferAmount = ethers.parseEther("50");
await (rToken.connect(user1).transfer(user2.address, transferAmount))
await (rToken.connect(user2).transfer(user3.address,ethers.parseEther("25")))
let balanceUser3 = await rToken.balanceOf(user3.address);
let scaledBalanceUser3 = await rToken.scaledBalanceOf(user3.address);
console.log("balanceUser3" , balanceUser3);
console.log("scaledBalanceUser3" , scaledBalanceUser3);
await mockLendingPool.mockMint(reservePool.address, user4.address, ethers.parseEther("25"), currentIndex);
let balanceUser4 = await rToken.balanceOf(user4.address);
let scaledBalanceUser4 = await rToken.scaledBalanceOf(user4.address);
console.log("balanceUser4" , balanceUser4);
console.log("currentIndex After" , currentIndex);
console.log("scaledBalanceUser4" , scaledBalanceUser4);
expect(scaledBalanceUser4).to.equal(scaledBalanceUser3);
expect(balanceUser4).to.equal(balanceUser3);
});
Impact
Users receive less than the expected amount when transferring RToken. Which result in loss of assets for receiver.
Tools Used
Manual Review, Unit Testing
Recommendations
Remove the redundant rayDiv operation from transfer() and transferFrom(), as it is already being done in _update().
- uint256 scaledAmount = amount.rayDiv(_liquidityIndex);
- return super.transferFrom(sender, recipient, scaledAmount);
+ return super.transferFrom(sender, recipient, amount);
- uint256 scaledAmount = amount.rayDiv(_liquidityIndex);
- return super.transfer(recipient, scaledAmount);
+ return super.transfer(recipient, amount);