Core Contracts

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

User will lost interests when calling transferFrom function in RToken

Summary

The RToken contract implements an interest-bearing token for the RAAC lending protocol. However, there is an inconsistency in the interest calculation between the transfer and transferFrom functions. While transfer uses the real-time normalized income from the ILendingPool contract to scale token amounts, transferFrom uses a fixed _liquidityIndex. This inconsistency can lead to users losing accrued interest when using transferFrom, as the fixed index may not reflect the latest interest rates.

Vulnerability Details

The transfer function scales the amount using ILendingPool(_reservePool).getNormalizedIncome(), which is a real-time value representing the current liquidity index (cumulative interest).

function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
return super.transfer(recipient, scaledAmount);
}

This ensures that the transferred amount is adjusted based on the latest interest rates, preventing users from losing accrued interest.

The transferFrom function scales the amount using _liquidityIndex, which is a fixed value representing the liquidity index at the time of the last update.

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

If _liquidityIndex is not updated frequently, users may lose accrued interest when using transferFrom, as the scaling does not reflect the latest interest rates.

POC

add the following test case into RToken.test.js

it("call transferFrom will lost interest", async function () {
let user3;
[, , , ,user3, ...addrs] = await ethers.getSigners();
const mintAmount = ethers.parseEther("100");
const index = RAY;
//crvUSD minted to user1
await mockLendingPool.mockMint(reservePool.address, user1.address, mintAmount, index);
//mock the interest increase
await mockLendingPool.mockGetNormalizedIncome(ethers.parseUnits("2", 27));
const transferAmount = ethers.parseEther("20");
await rToken.connect(user1).approve(user1.address,transferAmount);
await rToken.connect(user1).transfer(user2.address, transferAmount);
await rToken.connect(user1).transferFrom(user1.address,user3.address, transferAmount);
const balanceUser2 = await rToken.scaledBalanceOf(user2.address);
const balanceUser3 = await rToken.scaledBalanceOf(user3.address);
console.log("call transfer, the balanceUser2:",balanceUser2);
console.log("call transferFrom, the balanceUser3:",balanceUser3);
expect(balanceUser2).to.equal(balanceUser3);
});

run npx hardhat test --grep "call transferFrom will lost interest"

RToken
Basic functionality
call transfer, the balanceUser2: 5000000000000000000n
call transferFrom, the balanceUser3: 10000000000000000000n
1) call transferFrom will lost interest
0 passing (10s)
1 failing
1) RToken
Basic functionality
call transferFrom will lost interest:
AssertionError: expected 5000000000000000000 to equal 10000000000000000000.
+ expected - actual
-5000000000000000000
+10000000000000000000

After calling transferFrom, the scaledBalance is much smaller than expected.

Impact

  • Loss of Accrued Interest: Users who rely on transferFrom to transfer tokens may lose a portion of their accrued interest if _liquidityIndex is outdated.

  • Inconsistent Behavior: The inconsistency between transfer and transferFrom can lead to confusion and unexpected results for users.

The impact is High, the likelihood is High, so the severity is High.

Tools Used

Manual Review

Recommendations

Consider following fix:

function transferFrom(address sender, address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
return super.transferFrom(sender, recipient, scaledAmount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::transfer uses getNormalizedIncome() while transferFrom uses _liquidityIndex, creating inconsistent transfer amounts depending on function used

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::transfer uses getNormalizedIncome() while transferFrom uses _liquidityIndex, creating inconsistent transfer amounts depending on function used

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.