Core Contracts

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

Double Scaling Bug in RToken Implementation

Summary

The RToken contract contains a critical bug in its token transfer mechanism where amounts are incorrectly scaled twice by the liquidity index, resulting in users receiving fewer tokens than expected during transfers.

Vulnerability Details

The RToken contract implements an interest-bearing token system where token balances are scaled by a liquidity index to represent accrued interest. However, the current implementation contains a double scaling bug in the transfer process:

  1. First scaling occurs in the transfer 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);
}
  1. Second scaling occurs in the _update internal function:

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

The PoC demonstrates this issue by showing that when transferring 100 tokens with a liquidity index of 2 RAY:

  1. Initial balance of user1: 200 tokens (100 * 2)

  2. After transfer of 100 tokens to user2:

    • user1 balance: 150 tokens

    • user2 balance: 50 tokens

Expected behavior would be:

  • user1 balance: 100 tokens

  • user2 balance: 100 tokens

Impact

HIGH - The double scaling bug leads to:

  1. Users receiving fewer tokens than they should when receiving transfers

  2. Loss of value for users receiving transferred tokens

Tools Used

Manual Review

Unit test

Recommendations

  1. Remove the scaling operation from the transfer function since scaling is already handled in the _update.

PoC

Put this in RToken.test.js, and run the test npx hardhat test test/unit/core/tokens/RToken.test.js --grep "Transfer amount is not scaled correctly"

describe("Transfer amount is not scaled correctly", function() {
it("Transfer amount is not scaled correctly", async function () {
await mockLendingPool.setRToken(rToken.target);
const mintAmount = ethers.parseEther("100");
const mintOnBehalfOf = user1.address;
const index = RAY;
await mockLendingPool.mockMint(reservePool.address, mintOnBehalfOf, mintAmount, index);
// user1 has 100 rToken
console.log("Balances when index is RAY")
console.log(`RToken balance: user1: ${ethers.formatEther(await rToken.balanceOf(user1.address))}`);
console.log(`RToken balance: user2: ${ethers.formatEther(await rToken.balanceOf(user2.address))}`);
// simulate liquidity index increase over time
const index2 = RAY * BigInt(2);
await mockLendingPool.mockGetNormalizedIncome(index2);
console.log("Balances when index is RAY * 2")
console.log(`RToken balance: user1: ${ethers.formatEther(await rToken.balanceOf(user1.address))}`);
console.log(`RToken balance: user2: ${ethers.formatEther(await rToken.balanceOf(user2.address))}`);
await rToken.connect(user1).transfer(user2.address, ethers.parseEther("100"));
console.log("After Transfer");
console.log(`RToken balance: user1: ${ethers.formatEther(await rToken.balanceOf(user1.address))}`);
console.log(`RToken balance: user2: ${ethers.formatEther(await rToken.balanceOf(user2.address))}`);
});
});

Output

Balances when index is RAY
RToken balance: user1: 100.0
RToken balance: user2: 0.0
Balances when index is RAY * 2
RToken balance: user1: 200.0
RToken balance: user2: 0.0
After Transfer
RToken balance: user1: 150.0
RToken balance: user2: 50.0
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!