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