Core Contracts

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

RToken `transferFrom()` function updates balance incorrectly

Summary

The function RToken::transferFrom() uses stale liquidity index for calculation, which causes parties's balances to be updated incorrectly

Vulnerability Details

The state _liquidityIndex is set to WadRayMath.RAY in constructor and it can be updated by the function RToken::updateLiquidityIndex() which is meant to be called by LendingPool. Indeed, there is no function in the contract LendingPool that calls RToken::updateLiquidityIndex().

So, the function RToken::transferFrom() always use the stale value of _liquidityIndex = WadRayMath.RAY. As a result, the function _update() will update balances incorrectly

constructor(
string memory name,
string memory symbol,
address initialOwner,
address assetAddress
) ERC20(name, symbol) ERC20Permit(name) Ownable(initialOwner) {
if (initialOwner == address(0) || assetAddress == address(0)) revert InvalidAddress();
@> _liquidityIndex = WadRayMath.RAY;
_assetAddress = assetAddress;
}
function updateLiquidityIndex(uint256 newLiquidityIndex) external override onlyReservePool {
if (newLiquidityIndex < _liquidityIndex) revert InvalidAmount();
@> _liquidityIndex = newLiquidityIndex;
emit LiquidityIndexUpdated(newLiquidityIndex);
}
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);
}
function _update(address from, address to, uint256 amount) internal override {
// Scale amount by normalized income for all operations (mint, burn, transfer)
@> uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}

PoC

Add test to file test/unit/core/pools/StabilityPool/StabilityPool.test.js

describe("Deposits", function () {
...
it.only("transferFrom() updates balance incorrectly", async function() {
// @audit PoC RToken transferFrom() updates balance incorrectly
// borrow from lending pool
const tokenId = 1;
const price = ethers.parseEther("100");
await raacHousePrices.setOracle(owner.address);
await raacHousePrices.setHousePrice(tokenId, price);
await crvusd.mint(user1.address, price)
await crvusd.connect(user1).approve(raacNFT.target, price)
await raacNFT.connect(user1).mint(tokenId, price);
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
const borrowAmount = ethers.parseEther("50");
await lendingPool.connect(user1).borrow(borrowAmount);
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
await lendingPool.connect(user1).updateState();
console.log("\nbefore transfers")
let user1Balance = await rToken.balanceOf(user1.address);
let user3Balance = await rToken.balanceOf(user3.address);
console.log(`user1 ${user1Balance}\nuser3 ${user3Balance}`)
await rToken.connect(user2).transfer(user1.address, ethers.parseEther("10"))
await rToken.connect(user2).approve(user2.address, ethers.parseEther("10"))
await rToken.connect(user2).transferFrom(user2.address,user3.address, ethers.parseEther("10"))
user1Balance = await rToken.balanceOf(user1.address);
user3Balance = await rToken.balanceOf(user3.address);
console.log("\nafter transfers")
console.log(`user1 ${user1Balance}\nuser3 ${user3Balance}`)
})

Run the test and console shows:

StabilityPool
Core Functionality
Deposits
before transfers
user1 1000001171546476986016
user3 1000001171546476986016
after transfers
user1 1010001159831025941352
user3 1010001171546476986016

It means that the same amount is sent to user1 and user3 using transfer() and transferFrom() but it results different balances

Impact

  • Incorrect balance updates for users

Tools Used

Manual

Recommendations

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 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::updateLiquidityIndex() has onlyReservePool modifier but LendingPool never calls it, causing transferFrom() to use stale liquidity index values

Support

FAQs

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

Give us feedback!