Core Contracts

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

[H] Incorrect Descaling in `transfer` and `transferFrom` Functions in `RToken`

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:

  1. https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/RToken.sol#L224

  2. 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.


Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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 7 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.

Give us feedback!