Core Contracts

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

Wrong calculations on transfer functions in `RToken` contract

Summary

The RToken contract’s transfer functions apply an extra scaling division that, when combined with the internal update logic (which also scales the amount), results in double division. This leads to users transferring less tokens than intended.

Vulnerability Details

In the RToken contract, both the transfer and transferFrom functions include an explicit scaling division:

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

Under the hood, the ERC20 _update function (called during transfers) applies its own scaling. As a result, the amount is divided twice, and—for example—a transfer intending to move 20 tokens may only transfer 10 tokens.

Proof Of Concept
Add this to RToken.test.js

it("transfer wrong amount of rtokens", async function () {
const mintAmount = ethers.parseEther("100");
const index = RAY * ethers.toBigInt("2");
//crvUSD minted to user1
await mockLendingPool.mockMint(reservePool.address, user1.address, mintAmount, index);
await mockLendingPool.mockGetNormalizedIncome(index);
const scaledBalanceUser1BeforeTransfer = await rToken.balanceOf(user1.address);
const transferAmount = ethers.parseEther("20");
await rToken.connect(user1).transfer(user2.address, transferAmount)
// now since we have 100 tokens minted with the current index we know that our balance is 100
// we want to transfer 20 tokens so we should have 80 tokens left
// however there is a problem in the calculation as transfer applies scaling division once
// and then the update function also applies it
// so the amount transfered will be 20 * 1e27 / 2e27 = 10
const scaledBalanceUser1 = await rToken.balanceOf(user1.address);
const scaledBalanceUser2 = await rToken.balanceOf(user2.address);
expect(scaledBalanceUser1BeforeTransfer - scaledBalanceUser1).to.equal(ethers.parseEther("10"));
expect(scaledBalanceUser2).to.equal(ethers.parseEther("10"));
});

execute with npx hardhat test --grep "transfer wrong amount of rtokens"

Impact

  • Incorrect Token Transfers: Users will receive and send amounts lower than expected.

  • Liquidity Accounting Issues: Balances recorded on-chain will not reflect users’ true token entitlements, affecting further protocol operations.

Tools Used

  • Manual Code Review

  • Unit Testing

Recommendations

Remove the extra scaling division from the transfer and transferFrom functions. The internal update functions already handle the necessary scaling. For example, modify the functions as follows:

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.