The RToken contract has a critical double scaling vulnerability in its transfer implementation due to multiple scaling operations being applied to the same amount.
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/RToken.sol#L213
https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/RToken.sol#L309
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 _update(address from, address to, uint256 amount) internal override {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Double Scaling PoC", function () {
let rToken, mockLendingPool, owner, user1, user2;
const RAY = ethers.BigNumber.from("1000000000000000000000000000");
beforeEach(async function () {
[owner, user1, user2] = await ethers.getSigners();
const MockLendingPool = await ethers.getContractFactory("MockLendingPool");
mockLendingPool = await MockLendingPool.deploy();
await mockLendingPool.deployed();
await mockLendingPool.setNormalizedIncome(RAY.mul(2));
const RTokenFactory = await ethers.getContractFactory("RToken");
rToken = await RTokenFactory.deploy("RToken", "RT", owner.address, owner.address);
await rToken.deployed();
await rToken.setReservePool(mockLendingPool.address);
const amountUnderlying = ethers.utils.parseEther("1000");
await rToken.mint(owner.address, user1.address, amountUnderlying, RAY);
});
it("demonstrates that transfer uses double scaling", async function () {
const transferAmount = ethers.utils.parseEther("500");
await rToken.connect(user1).transfer(user2.address, transferAmount);
const user2Balance = await rToken.balanceOf(user2.address);
console.log("User2 scaled balance:", user2Balance.toString());
expect(user2Balance.lt(transferAmount)).to.be.true;
});
});