Core Contracts

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

Double scaling in RToken transfer functions leads to incorrect token transfers

Summary

The RToken contract's transfer function incorrectly scale amounts twice by dividing by the liquidity index in both the transfer() function and the internal _update() function, resulting in users receiving half of the intended transfer amount.

Vulnerability Details

The RToken::transfer function attempt to scale the transfer amount by dividing it by the liquidity index to convert from virtual token amount to the actual scaled token units. However, this function call super.transfer(), which internally call the overridden _update() function that also performs the same scaling operation.

function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome()); // First scaling
return super.transfer(recipient, scaledAmount); // Calls _update which scales again
}
function _update(address from, address to, uint256 amount) internal override {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome()); // Second scaling
super._update(from, to, scaledAmount);
}

Proof of Concept:

Add the following test to the RToken.test.js file:

describe("Incorrect transfer/transferFrom when liquidity index is not 1", function () {
beforeEach(async function () {
// Set rToken in mock
await mockLendingPool.setRToken(rToken.target);
});
it("should not work correctly when transferring with a different liquidity index", async function () {
const mintAmount = ethers.parseEther("100");
const mintOnBehalfOf = user1.address;
const index = RAY;
await expect(mockLendingPool.mockMint(reservePool.address, mintOnBehalfOf, mintAmount, index))
.to.emit(rToken, "Mint")
.withArgs(reservePool.address, mintOnBehalfOf, mintAmount, index);
const user1BalanceBeforeLiquidityIndexIncrease = await rToken.balanceOf(user1.address);
expect(user1BalanceBeforeLiquidityIndexIncrease).to.equal(ethers.parseEther("100"));
// Set the normalized income (liquidity index) to 2
await mockLendingPool.mockGetNormalizedIncome(ethers.parseUnits("2", 27));
// The user now should have 200 rTokens, because the liquidity index is 2
const user1Balance = await rToken.balanceOf(user1.address);
expect(user1Balance).to.equal(ethers.parseEther("200"));
const user2Balance = await rToken.balanceOf(user2.address);
expect(user2Balance).to.equal(0);
// Try to transfer the whole balance of RToken from user1 to user2
await rToken.connect(user1).transfer(user2.address, user1Balance);
const user1BalanceAfter = await rToken.balanceOf(user1.address);
const user2BalanceAfter = await rToken.balanceOf(user2.address);
expect(user1BalanceAfter).to.equal(0); // It is 100 ether
expect(user2BalanceAfter).to.equal(user1Balance); // It is 100 ether
});
});

The test above will fail, because the user1BalanceAfter is 100 ether, instead of 0, and the user2BalanceAfter is 100 ether, instead of 200 ether. This can be seen by the logs in the console.

Impact

  • Users less than the intended transfer amount

  • Users cannot transfer their full token balance because it will be scaled down twice

Recommendations

Remove scaling from transfer functions and rely only on _update:

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);
}
Updates

Lead Judging Commences

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