Summary
The transfer and transferFrom functions in the RToken contract incorrectly descale the amount before passing it to the parent functions. The issue is that descaling is already happening in the _update function, leading to incorrect balance transfers.
Vulnerability Details
The current transfer function in the RToken contract is:
function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
return super.transfer(recipient, scaledAmount);
}
The current transferFrom function in the RToken contract is:
function transferFrom(address sender, address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(_liquidityIndex);
return super.transferFrom(sender, recipient, scaledAmount);
}
Both functions descale the amount before passing it to the parent functions. However, descaling is again happening in the overriden _update function:
function _update(address from, address to, uint256 amount) internal override {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}
This leads to double descaling and incorrect balance transfers.
POC
Copy Paste this test in LendingPool.test.js in Borrow and Repay block. The lending Pool withdraw functions used on how users can cheat others by selling incorrect amount of RToken.
it.only("Incorrect Transfer and trasferFrom", async function () {
const borrowAmount = ethers.parseEther("50");
await lendingPool.connect(user1).borrow(borrowAmount);
const debtAmount = await debtToken.balanceOf(user1.address);
await crvusd
.connect(user1)
.approve(rToken.target, debtAmount + debtAmount);
await mine(100, { interval: 12 });
await lendingPool
.connect(user1)
.repay(debtAmount + debtAmount);
const index = await lendingPool.getNormalizedIncome();
console.log("index", ethers.formatUnits(index, 27));
console.log("rToken balance of user1 before transfer", formatEther(await rToken.balanceOf(user1.address)));
console.log("rToken balance of user2 before transfer", formatEther(await rToken.balanceOf(user2.address)));
const actual_balance_transfer = await rToken.balanceOf(user2.address);
rToken.connect(user2).transfer(user1.address, actual_balance_transfer);
await rToken.balanceOf(user1.address);
await rToken.balanceOf(user1.address);
console.log("rToken balance of user1 after transfer", formatEther(await rToken.balanceOf(user1.address)));
const balance_received = await rToken.balanceOf(user1.address);
expect(balance_received).to.lessThan(actual_balance_transfer);
console.log("rToken balance of user2 after transfer", formatEther(await rToken.balanceOf(user2.address)));
console.log("balcneces before withdraw crv u1",formatEther(await crvusd.balanceOf(user1.address)))
console.log("balcneces before withdraw crv u2",formatEther(await crvusd.balanceOf(user2.address)))
await lendingPool.connect(user1).withdraw(await rToken.balanceOf(user1.address));
await lendingPool.connect(user2).withdraw(await rToken.balanceOf(user2.address));
console.log("balances after withdraw crv u1",formatEther(await crvusd.balanceOf(user1.address)))
console.log("balances after withdraw crv u2",formatEther(await crvusd.balanceOf(user2.address)))
});
the output of the test looks like
LendingPool
Borrow and Repay
index 1.000000025816848936177903431
rToken balance of user1 before transfer 0.0
rToken balance of user2 before transfer 2000.000051633697872356
rToken balance of user1 after transfer 2000.0
rToken balance of user2 after transfer 0.000051633697872355
balcneces before withdraw crv u1 999.999948366274988339
balcneces before withdraw crv u2 8000.0
balances after withdraw crv u1 2999.999948366274988339
balances after withdraw crv u2 8000.000051633697872355
With linearly increasing of usageIndex of the proptocl the difference will be more
Linsk to the issues:
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/RToken.sol#L224
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/RToken.sol#L213
Impact
This issue can lead to incorrect balance transfers, potentially causing issues with token accounting and user balnces and in stability pool there we have to lock this to get RAAC which cause accounting errors @remind to write POC for this.
Tools Used
Manual code review.
Recommendations
Remove the descaling in the transfer and transferFrom functions to ensure that the amount is only descaled once in the _update function. The corrected functions should be:
Corrected transfer Function
function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
return super.transfer(recipient, amount);
}
Corrected transferFrom Function
function transferFrom(address sender, address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
return super.transferFrom(sender, recipient, amount);
}
This ensures that the amount is only descaled once, leading to correct balance transfers.