Core Contracts

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

Double down scaling in `RToken` transfer functions results in incorrect token amounts transferred

Summary

The RToken contract performs double ray division on transfer amounts, once in the transfer/transferFrom functions and again in the internal _update function, leading to severely reduced transfer amounts.

Vulnerability Details

In RToken.sol, both the transfer and transferFrom functions perform a ray division on the input amount:

function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
return super.transfer(recipient, scaledAmount);
}
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);
}

However, the internal _update function also performs the same ray division:

function _update(address from, address to, uint256 amount) internal override {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}

This results in the amount being divided twice by the normalized income, reducing the actual transfer amount.

Impact

The double division causes all transfers to move significantly fewer tokens than intended. For example, if a user attempts to transfer 100 tokens with a normalized income of 2 RAY:

  • First division: 100 / 2 = 50

  • Second division: 50 / 2 = 25
    The recipient receives only 25 tokens instead of the intended 100 tokens, effectively losing 75% of the value. This affects all token transfers in the system, making the token unusable for its intended purpose.

Tools Used

Manual review

Proof of Concept

Add the following test case to the test/unit/core/tokens/RToken.test.js file inside the Basic functionality describe block:

it("Transfer double scaled amount", async function () {
const balancebeforeUser1 = await rToken.balanceOf(user1.address);
const balancebeforeUser2 = await rToken.balanceOf(user2.address);
const newIndex = RAY + RAY; // double the index
await mockLendingPool.mockGetNormalizedIncome(newIndex);
// Transfer half of the balance
const transferAmount = balancebeforeUser1 / 2n;
await rToken.connect(user1).transfer(user2.address, transferAmount);
const user1BalanceAfterTransfer = await rToken.balanceOf(user1.address);
const user2BalanceAfterTransfer = await rToken.balanceOf(user2.address);
// These are the correct expect but they fail because of the double scaled amount
// expect(user1BalanceAfterTransfer).to.equal(balancebeforeUser1 - transferAmount);
// expect(user2BalanceAfterTransfer).to.equal(balancebeforeUser2 + transferAmount);
expect(user1BalanceAfterTransfer).to.equal(balancebeforeUser1 - transferAmount / 2n);
expect(user2BalanceAfterTransfer).to.equal(balancebeforeUser2 + transferAmount / 2n);
});

Recommendations

Remove the ray division from the transfer and transferFrom functions, keeping it only in the _update function:

function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
- uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
- return super.transfer(recipient, scaledAmount);
+ return super.transfer(recipient, amount);
}
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);
+ return super.transferFrom(sender, recipient, amount);
}
Updates

Lead Judging Commences

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